@@ -128,8 +128,9 @@ defmodule HTTP.FormData do
128128 Converts FormData to HTTP body content with appropriate encoding.
129129
130130 Returns {:url_encoded, body} for regular forms or {:multipart, body, boundary} for multipart.
131+ The multipart body is returned as iodata for memory efficiency with large file uploads.
131132 """
132- @ spec to_body ( t ( ) ) :: { :url_encoded , String . t ( ) } | { :multipart , String . t ( ) , String . t ( ) }
133+ @ spec to_body ( t ( ) ) :: { :url_encoded , String . t ( ) } | { :multipart , iodata ( ) , String . t ( ) }
133134 def to_body ( % __MODULE__ { parts: parts } = form ) do
134135 has_file? =
135136 Enum . any? ( parts , fn
@@ -184,33 +185,38 @@ defmodule HTTP.FormData do
184185
185186 body_parts =
186187 parts
187- |> Enum . map ( fn
188+ |> Enum . flat_map ( fn
188189 { :field , name , value } ->
189- encode_multipart_field ( boundary , name , value )
190+ [ encode_multipart_field ( boundary , name , value ) , " \r \n " ]
190191
191192 { :file , name , filename , content_type , % File.Stream { } = stream } ->
192- encode_multipart_file_stream ( boundary , name , filename , content_type , stream )
193+ [ encode_multipart_file_stream ( boundary , name , filename , content_type , stream ) , " \r \n " ]
193194
194195 { :file , name , filename , content_type , content } ->
195- encode_multipart_file_content ( boundary , name , filename , content_type , content )
196+ [ encode_multipart_file_content ( boundary , name , filename , content_type , content ) , " \r \n " ]
196197 end )
197198
198- body = Enum . join ( body_parts , "\r \n " ) <> "\r \n --" <> boundary <> "--\r \n "
199+ # Build as iodata list for memory efficiency
200+ body = [ body_parts , "--" , boundary , "--\r \n " ]
199201
200202 { :multipart , body , boundary }
201203 end
202204
203205 defp encode_multipart_field ( boundary , name , value ) do
204- "--#{ boundary } \r \n " <>
205- "Content-Disposition: form-data; name=\" #{ name } \" \r \n \r \n " <>
206- "#{ value } "
206+ [
207+ "--#{ boundary } \r \n " ,
208+ "Content-Disposition: form-data; name=\" #{ name } \" \r \n \r \n " ,
209+ value
210+ ]
207211 end
208212
209213 defp encode_multipart_file_content ( boundary , name , filename , content_type , content ) do
210- "--#{ boundary } \r \n " <>
211- "Content-Disposition: form-data; name=\" #{ name } \" ; filename=\" #{ filename } \" \r \n " <>
212- "Content-Type: #{ content_type } \r \n \r \n " <>
214+ [
215+ "--#{ boundary } \r \n " ,
216+ "Content-Disposition: form-data; name=\" #{ name } \" ; filename=\" #{ filename } \" \r \n " ,
217+ "Content-Type: #{ content_type } \r \n \r \n " ,
213218 content
219+ ]
214220 end
215221
216222 defp encode_multipart_file_stream (
@@ -220,7 +226,9 @@ defmodule HTTP.FormData do
220226 content_type ,
221227 % File.Stream { } = stream
222228 ) do
223- content = stream |> Enum . into ( "" )
224- encode_multipart_file_content ( boundary , name , filename , content_type , content )
229+ # Convert stream to list of binaries (iodata) instead of concatenating into single binary
230+ # This avoids loading the entire file into memory as one large binary
231+ content_chunks = Enum . to_list ( stream )
232+ encode_multipart_file_content ( boundary , name , filename , content_type , content_chunks )
225233 end
226234end
0 commit comments