{"record":{"id":"8a004910e89e477c","repo":"ruby-grape/grape","slug":"stream-object-must-respond-to-each","errorCode":null,"errorMessage":"Stream object must respond to :each.","messagePattern":"Stream object must respond to :each\\.","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/grape/dsl/inside_route.rb","lineNumber":188,"sourceCode":"      def http_version\n        env.fetch('HTTP_VERSION') { env[Rack::SERVER_PROTOCOL] }\n      end\n\n      def api_format(format)\n        env[Grape::Env::API_FORMAT] = format\n      end\n\n      def context\n        self\n      end\n\n      private\n\n      # Wraps a stream +value+ into a body that responds to +:each+.\n      def stream_body(value)\n        return Grape::ServeStream::FileBody.new(value) if value.is_a?(String)\n\n        raise ArgumentError, 'Stream object must respond to :each.' unless value.respond_to?(:each)\n\n        value\n      end\n\n      # The default HTTP status when none has been set explicitly.\n      def default_status\n        return 201 if request.post?\n        return 204 if request.delete? && @body.blank?\n\n        200\n      end\n    end\n  end\nend\n","sourceCodeStart":170,"sourceCodeEnd":203,"githubUrl":"https://github.com/ruby-grape/grape/blob/22d7975629846a3c0c7bd2b34e140a7a1b4af8f6/lib/grape/dsl/inside_route.rb#L170-L203","documentation":"`stream_body` (used by the `stream` helper) wraps the response body for chunked streaming. Rack streams a body by calling `each`, so any non-String value passed here must respond to `each` - an Enumerator, an Enumerable, or a custom object implementing `each`. A String is accepted because FileBody treats it as a path; anything else lacking `each` raises 'Stream object must respond to :each.'.","triggerScenarios":"`stream_body 42` or `stream_body some_struct` with a plain value object. `stream` with a wrapper object that forgot to implement `each` (e.g. a lazy generator exposing only `next`). Passing a Proc or lambda instead of an Enumerator.","commonSituations":"Streaming SSE/NDJSON endpoints using custom cursor or query objects. Wrapping a database cursor for chunked responses without including Enumerable. Refactoring from returning an array to a custom iterator.","solutions":["Pass an Enumerator, e.g. `stream_body Enumerator.new { |y| ... }` or `stream_body collection.each`.","Implement `each` (and ideally include Enumerable) on the custom stream object.","If the value is actually a file path, pass the String and let FileBody handle it."],"exampleFix":"# before\nclass Cursor\n  def initialize(rows) = @rows = rows\n  def next = @rows.shift\nend\nstream_body Cursor.new(rows) # raises, no :each\n\n# after\nclass Cursor\n  include Enumerable\n  def initialize(rows) = @rows = rows\n  def each(&) = @rows.each(&)\nend\nstream_body Cursor.new(rows)","handlingStrategy":"type-guard","validationCode":"streamable = value.is_a?(String) || value.respond_to?(:each)\nraise ArgumentError, \"#{value.class} cannot stream (needs :each or a path String)\" unless streamable\nstream_body value","typeGuard":"def streamable?(value) = value.is_a?(String) || value.respond_to?(:each)","tryCatchPattern":null,"preventionTips":["Give custom iterators `include Enumerable` plus `each` by construction.","Prefer Enumerators (`Enumerator.new`, `collection.each`) for generated streams.","Spec streaming endpoints with a body-consuming assertion so a non-each object fails tests."],"tags":["grape","stream","stream-body","enumerable","response"],"backgroundTag":"non-enumerable-stream-body","analyzedSha":"22d7975629846a3c0c7bd2b34e140a7a1b4af8f6","analyzedAt":"2026-08-21T17:03:54.627Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}