@@ -212,6 +212,20 @@ defmodule HTTP do
212212 | { { :http_version , integer ( ) , String . t ( ) } , [ { atom ( ) | String . t ( ) , String . t ( ) } ] ,
213213 binary ( ) }
214214
215+
216+ defp handle_response ( request_id , url ) do
217+ receive do
218+ { :http , { ^ request_id , response_from_httpc } } ->
219+ response = handle_httpc_response ( response_from_httpc , url )
220+ { :ok , response }
221+ _ ->
222+ throw ( :request_interrupted_or_unexpected_message )
223+ after
224+ 120_000 ->
225+ throw ( :request_timeout )
226+ end
227+ end
228+
215229 # Internal function, not part of public API
216230 @ doc false
217231 @ spec handle_async_request (
@@ -224,35 +238,23 @@ defmodule HTTP do
224238 try do
225239 case Request . to_httpc_args ( request ) do
226240 [ method , request_tuple , options , client_options ] ->
227- # Send the request and get the RequestId (PID of the httpc client process)
228- case :httpc . request ( method , request_tuple , options , client_options ) do
229- { :ok , request_id } ->
230- # If an AbortController was provided, link it to this request_id
231- if abort_controller_pid && is_pid ( abort_controller_pid ) do
232- HTTP.AbortController . set_request_id ( abort_controller_pid , request_id )
233- end
234-
235- # Now, receive the response message from :httpc
236- # The message format is {:httpc, {RequestId, ResponseTuple}}
237- # Default 2 minute timeout if no response received
238- receive do
239- { :http , { ^ request_id , response_from_httpc } } ->
240- # This will return %Response{} or throw
241- response = handle_httpc_response ( response_from_httpc , request . url )
242- # Wrap in :ok for the Task result
243- { :ok , response }
244-
245- _ ->
246- # This catch-all can happen if the process is killed or another message arrives
247- throw ( :request_interrupted_or_unexpected_message )
248- after
249- 120_000 ->
250- throw ( :request_timeout )
251- end
241+ # Configure httpc options
242+ httpc_options = Keyword . put ( options , :body_format , :binary )
243+
244+ # Send the request and get the RequestId (PID of the httpc client process)
245+ case :httpc . request ( method , request_tuple , httpc_options , client_options ) do
246+ { :ok , request_id } ->
247+ # If an AbortController was provided, link it to this request_id
248+ if abort_controller_pid && is_pid ( abort_controller_pid ) do
249+ HTTP.AbortController . set_request_id ( abort_controller_pid , request_id )
250+ end
252251
253- { :error , reason } ->
254- throw ( reason )
255- end
252+ # Handle response (simplified - streaming handled in handle_httpc_response)
253+ handle_response ( request_id , request . url )
254+
255+ { :error , reason } ->
256+ throw ( reason )
257+ end
256258
257259 # Fallback for unexpected return from Request.to_httpc_args
258260 other_args ->
@@ -266,33 +268,111 @@ defmodule HTTP do
266268
267269 # Success case: returns %Response{} directly
268270 @ spec handle_httpc_response ( httpc_response_tuple ( ) , String . t ( ) | nil ) :: Response . t ( )
269- defp handle_httpc_response ( { { _version , status , _reason_phrase } , httpc_headers , body } , url ) do
270- # Convert :httpc's header list to HTTP.Headers struct
271- response_headers =
272- httpc_headers
273- |> Enum . map ( fn { key , val } -> { to_string ( key ) , to_string ( val ) } end )
274- |> HTTP.Headers . new ( )
275-
276- # Convert body from charlist (iodata) to binary if it's not already
277- binary_body =
278- if is_list ( body ) do
279- IO . iodata_to_binary ( body )
280- else
281- body
282- end
271+ defp handle_httpc_response ( response_tuple , url ) do
272+ case response_tuple do
273+ { { _version , status , _reason_phrase } , httpc_headers , body } ->
274+ # Convert :httpc's header list to HTTP.Headers struct
275+ response_headers =
276+ httpc_headers
277+ |> Enum . map ( fn { key , val } -> { to_string ( key ) , to_string ( val ) } end )
278+ |> HTTP.Headers . new ( )
279+
280+ # Check if we should use streaming
281+ content_length = HTTP.Headers . get ( response_headers , "content-length" )
282+ should_stream = should_use_streaming? ( content_length )
283+
284+ if should_stream do
285+ # Create a streaming process
286+ { :ok , stream_pid } = start_httpc_stream_process ( url , response_headers )
287+ % Response { status: status , headers: response_headers , body: nil , url: url , stream: stream_pid }
288+ else
289+ # Non-streaming response - handle as before
290+ binary_body =
291+ if is_list ( body ) do
292+ IO . iodata_to_binary ( body )
293+ else
294+ body
295+ end
296+ % Response { status: status , headers: response_headers , body: binary_body , url: url , stream: nil }
297+ end
298+
299+ { :error , reason } ->
300+ throw ( reason )
283301
284- % Response { status: status , headers: response_headers , body: binary_body , url: url }
302+ other ->
303+ throw ( { :unexpected_response , other } )
304+ end
285305 end
286306
287- # Error case: throws the reason
288- @ spec handle_httpc_response ( { :error , term ( ) } , String . t ( ) | nil ) :: no_return ( )
289- defp handle_httpc_response ( { :error , reason } , _original_url ) do
290- throw ( reason )
307+ defp should_use_streaming? ( content_length ) do
308+ # Stream responses larger than 100KB or when content-length is unknown
309+ case Integer . parse ( content_length || "" ) do
310+ { size , _ } when size > 100_000 -> true
311+ _ -> content_length == nil # Stream when size is unknown
312+ end
291313 end
292314
293- # Unexpected response case: throws an explicit error
294- @ spec handle_httpc_response ( term ( ) , String . t ( ) | nil ) :: no_return ( )
295- defp handle_httpc_response ( other , _original_url ) do
296- throw ( { :unexpected_response , other } )
315+ defp start_httpc_stream_process ( url , headers ) do
316+ { :ok , pid } = Task . start_link ( fn ->
317+ stream_httpc_response ( url , headers )
318+ end )
319+ { :ok , pid }
297320 end
321+
322+ defp stream_httpc_response ( url , headers ) do
323+ # Create a streaming request using :httpc
324+ uri = URI . parse ( url )
325+ _host = uri . host
326+ _port = uri . port || 80
327+ _path = uri . path || "/"
328+
329+ # Build headers for the request
330+ request_headers =
331+ headers . headers
332+ |> Enum . map ( fn { name , value } -> { String . to_charlist ( name ) , String . to_charlist ( value ) } end )
333+
334+ # Start the HTTP request with streaming
335+ case :httpc . request (
336+ :get ,
337+ { String . to_charlist ( url ) , request_headers } ,
338+ [ ] ,
339+ [ sync: false , body_format: :binary ]
340+ ) do
341+ { :ok , request_id } ->
342+ stream_loop ( request_id , self ( ) )
343+ { :error , reason } ->
344+ send ( self ( ) , { :stream_error , self ( ) , reason } )
345+ end
346+ end
347+
348+ defp stream_loop ( request_id , caller ) do
349+ receive do
350+ { :http , { ^ request_id , { :http_response , _http_version , _status , _reason } } } ->
351+ stream_loop ( request_id , caller )
352+
353+ { :http , { ^ request_id , { :http_header , _ , _header_name , _ , _header_value } } } ->
354+ stream_loop ( request_id , caller )
355+
356+ { :http , { ^ request_id , :http_eoh } } ->
357+ stream_loop ( request_id , caller )
358+
359+ { :http , { ^ request_id , { :http_error , reason } } } ->
360+ send ( caller , { :stream_error , self ( ) , reason } )
361+
362+ { :http , { ^ request_id , :stream_end } } ->
363+ send ( caller , { :stream_end , self ( ) } )
364+
365+ { :http , { ^ request_id , { :http_chunk , chunk } } } ->
366+ send ( caller , { :stream_chunk , self ( ) , to_string ( chunk ) } )
367+ stream_loop ( request_id , caller )
368+
369+ { :http , { ^ request_id , { :http_body , body } } } ->
370+ send ( caller , { :stream_chunk , self ( ) , to_string ( body ) } )
371+ send ( caller , { :stream_end , self ( ) } )
372+
373+ after 60_000 ->
374+ send ( caller , { :stream_error , self ( ) , :timeout } )
375+ end
376+ end
377+
298378end
0 commit comments