streamedAjax function

Future<Response> streamedAjax(
  1. Uri url,
  2. Future send(
    1. EventSink<List<int>> sink
    ), {
  3. String method = "GET",
  4. Map<String, String>? headers,
  5. int? contentLength,
  6. Duration? timeout,
})

Sends a request with a streamed body and returns the response.

The streaming counterpart to ajax: instead of buffering the request body up front, the caller writes bytes into the EventSink passed to send (e.g. by piping an inbound HTTP request stream via package:stream's copyToSink). The sink is closed automatically when send returns or throws.

HEAD is not supported — use headAjax instead. This function materializes the response via http.Response.fromStream, which would lose the wire's Content-Length for HEAD (the body is empty by definition); headAjax handles this case correctly.

  • contentLength — when known up front, set it so the outbound request is sent fixed-length. If omitted, the request is sent Transfer-Encoding: chunked (some endpoints, e.g. S3 PUT under SigV2, reject chunked bodies).

  • timeout — bounds the entire request. If exceeded, the underlying HttpClient is force-closed and TimeoutException is thrown.

Implementation

Future<http.Response> streamedAjax(Uri url,
    Future Function(EventSink<List<int>> sink) send,
    {String method = "GET", Map<String, String>? headers,
     int? contentLength, Duration? timeout}) {
  if (method.toUpperCase() == "HEAD")
    throw ArgumentError('HEAD: use headAjax');

  final httpClient = HttpClient();
  final client = http_io.IOClient(httpClient);

  Future<http.Response> doIt() async {
    try {
      final request = http.StreamedRequest(method, url);
      headers?.forEach((k, v) {
        //Promote `Content-Length` to `.contentLength`; left in the map
        //alone the request goes out as chunked.
        if (k.toLowerCase() == 'content-length') {
          assert(int.tryParse(v) != null, 'Content-Length: $v');
          request.contentLength ??= int.tryParse(v);
        } else {
          request.headers[k] = v;
        }
      });
      if (contentLength != null) request.contentLength = contentLength;

      //Drive the body before awaiting the response: `client.send` only
      //resolves after the body stream emits done.
      final responseFuture = client.send(request);
      responseFuture.ignore();
      try {
        await send(request.sink);
      } finally {
        request.sink.close();
      }
      return await http.Response.fromStream(await responseFuture);
    } finally {
      InvokeUtil.invokeSafely(client.close);
    }
  }

  final inner = doIt();
  if (timeout == null) return inner;

  return inner.timeout(timeout, onTimeout: () {
    httpClient.close(force: true);
    inner.ignore();
    throw TimeoutException('streamedAjax($method $url)', timeout);
  });
}