Skip to content

Commit 3da0bc0

Browse files
Add withStreamBody
Fetch needs `duplex: half` when setting a stream request body. https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests#half_duplex This also adds an error case to the legacy streams api when configuring a request with `withStreamBody`
1 parent fc7a917 commit 3da0bc0

File tree

3 files changed

+120
-3
lines changed

3 files changed

+120
-3
lines changed

integration-tests/http-client/src/Tests.gren

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,70 @@ suite permission =
139139
test "succeeds with empty record"
140140
(\_ -> Expect.equal {} res.data)
141141
)
142+
, await
143+
(Stream.fromArray [ meaningOfLifeInBytes ]
144+
|> Task.mapError StreamErr
145+
|> Task.andThen
146+
(\body ->
147+
HttpClient.post "/echo"
148+
|> HttpClient.withStreamBody "application/octet-stream" body
149+
|> HttpClient.expectBytes
150+
|> send
151+
|> Task.mapError HttpErr
152+
)
153+
)
154+
"with streaming request body"
155+
(\res ->
156+
test "streams request body" (\_ -> Expect.equal meaningOfLifeInBytes res.data)
157+
)
158+
, await
159+
(Stream.identityTransformation
160+
|> Task.andThen
161+
(\transform ->
162+
Stream.writable transform
163+
|> Stream.writeStringAsBytes "Hello!"
164+
|> Task.andThen Stream.closeWritable
165+
|> Task.map (\_ -> Stream.readable transform)
166+
|> Task.mapError StreamErr
167+
)
168+
|> Task.andThen
169+
(\body ->
170+
HttpClient.post "/echo"
171+
|> HttpClient.withStreamBody "application/octet-stream" body
172+
|> HttpClient.expectString
173+
|> HttpClient.send permission
174+
|> Task.mapError HttpErr
175+
)
176+
)
177+
"with writable stream"
178+
(\res ->
179+
test "streams request body" (\_ -> Expect.equal "Hello!" res.data)
180+
)
181+
, await
182+
(let
183+
request1 : Task Error (HttpClient.Response (Stream.Readable Bytes))
184+
request1 =
185+
HttpClient.post "/echo"
186+
|> HttpClient.withBytesBody "application/octet-stream" meaningOfLifeInBytes
187+
|> HttpClient.expectStream
188+
|> send
189+
|> Task.mapError HttpErr
190+
191+
request2 : Stream.Readable Bytes -> Task Error (HttpClient.Response Bytes)
192+
request2 streamBody =
193+
HttpClient.post "/echo"
194+
|> HttpClient.withStreamBody "application/octet-stream" streamBody
195+
|> HttpClient.expectBytes
196+
|> send
197+
|> Task.mapError HttpErr
198+
in
199+
request1
200+
|> Task.andThen (\res -> request2 res.data)
201+
)
202+
"with piped streaming requests"
203+
(\res ->
204+
test "receives bytes through piped request chain" (\_ -> Expect.equal meaningOfLifeInBytes res.data)
205+
)
142206
, await
143207
(HttpClient.post "/echo"
144208
|> HttpClient.withBytesBody "application/octet-stream" meaningOfLifeInBytes

src/Gren/Kernel/HttpClient.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ var _HttpClient_request = function (config) {
4040
{},
4141
config.__$headers,
4242
),
43+
duplex: "half",
4344
body: _HttpClient_extractRequestBody(config),
4445
signal: controller.signal,
4546
})
@@ -189,7 +190,13 @@ var _HttpClient_stream = F4(function (cleanup, sendToApp, request, config) {
189190

190191
const body = _HttpClient_extractRequestBody(config);
191192

192-
if (body == null) {
193+
if (config.__$bodyType === "STREAM") {
194+
send(
195+
__HttpClient_UnknownError(
196+
"stream request body not supported in legacy api",
197+
),
198+
);
199+
} else if (body == null) {
193200
send(__HttpClient_SentChunk(request));
194201
} else {
195202
req.write(body, () => {
@@ -303,6 +310,8 @@ var _HttpClient_extractRequestBody = function (config) {
303310
return config.__$body.a;
304311
case "BYTES":
305312
return _HttpClient_prepBytes(config.__$body.a);
313+
case "STREAM":
314+
return config.__$body.a;
306315
}
307316
};
308317

src/HttpClient.gren

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ effect module HttpClient where { command = MyCmd } exposing
44
, RequestConfiguration, get, post, request
55
, defaultTimeout, withTimeout
66
, withHeader, withDuplicatedHeader
7-
, Body, withEmptyBody, withStringBody, withJsonBody, withBytesBody
7+
, Body, withEmptyBody, withStringBody, withJsonBody, withBytesBody, withStreamBody
88
, Expect, expectAnything, expectNothing, expectString, expectJson, expectBytes, expectStream
99
, send
1010
, Response
@@ -55,7 +55,7 @@ It might be interesting to read this [list of HTTP header fields](https://en.wik
5555

5656
The request body is the actual data that you wish to send to a server.
5757

58-
@docs Body, withEmptyBody, withStringBody, withJsonBody, withBytesBody
58+
@docs Body, withEmptyBody, withStringBody, withJsonBody, withBytesBody, withStreamBody
5959

6060
## Expected response body
6161

@@ -78,6 +78,8 @@ Once your `Response` is configured, you'll want to actually send the request.
7878

7979
## Streaming
8080

81+
**DEPRECATED** - this api will be removed in the next major version, prefer using the newer [withStreamBody](#withStreamBody) and [expectStream](#expectStream) for streaming requests.
82+
8183
Streaming is the more advanced way to perform a HTTP request. This requires that you follow the Elm
8284
architecture, as you'll receive messages for every chunk of data sent and received. The benefit of this
8385
extra complexity, is that you can perform actions while the request is being performed.
@@ -225,6 +227,7 @@ type Body
225227
= BodyEmpty
226228
| BodyString String
227229
| BodyBytes Bytes
230+
| BodyStream (Stream.Readable Bytes)
228231

229232

230233
bodyTypeAsString : Body -> String
@@ -239,6 +242,9 @@ bodyTypeAsString body =
239242
BodyBytes _ ->
240243
"BYTES"
241244

245+
BodyStream _ ->
246+
"STREAM"
247+
242248

243249
{-| Removes the body from the [RequestConfiguration](#RequestConfiguration).
244250
You normally don't have to use this function, as an empty body is the default.
@@ -286,6 +292,44 @@ withBytesBody mimeType value req =
286292
}
287293

288294

295+
{-| Sets the provided Readable Bytes stream as the request body. You need to provide a mime type to
296+
desribe what the bytes represent. This mime type will be set as the "Content-Type" header,
297+
potentially overwriting the header if it has already been set.
298+
299+
You could use a stream body for sending a large file in an http request for example:
300+
301+
import HttpClient
302+
import FileSystem
303+
import FileSystem.Path as Path
304+
305+
306+
type Error
307+
= FsErr FileSystem.Error
308+
| HttpErr HttpClient.Error
309+
310+
311+
upload : HttpClient.Permission -> FileSystem.Permission -> Task Error (HttpClient.Response {})
312+
upload httpPerm fsPerm =
313+
Path.fromPosixString "/large-file.csv"
314+
|> FileSystem.readFileStream fsPerm FileSystem.Beginning
315+
|> Task.mapError FsErr
316+
|> Task.andThen (\fileStream ->
317+
HttpClient.post "/upload"
318+
|> HttpClient.withStreamBody fileStream
319+
|> HttpClient.send httpPerm
320+
|> Task.mapError HttpErr
321+
)
322+
323+
-}
324+
withStreamBody : String -> Stream.Readable Bytes -> RequestConfiguration a -> RequestConfiguration a
325+
withStreamBody mimeType value req =
326+
{ req
327+
| headers = Dict.set "content-type" [mimeType] req.headers
328+
, body = BodyStream value
329+
}
330+
331+
332+
289333
-- EXPECT
290334

291335

0 commit comments

Comments
 (0)