send method

  1. @override
Future<StreamedResponse> send(
  1. BaseRequest request
)

Sends an HTTP request and asynchronously returns the response.

Implementers should call BaseRequest.finalize to get the body of the request as a ByteStream. They shouldn't make any assumptions about the state of the stream; it could have data written to it asynchronously at a later point, or it could already be closed when it's returned. Any internal HTTP errors should be wrapped as ClientExceptions.

Implementation

@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
  if (_isClosed) {
    throw http.ClientException(
      'HTTP request failed. Client is already closed.',
      request.url,
    );
  }

  if (!request.url.hasScheme || !request.url.hasAuthority) {
    throw http.ClientException(
      'Request URL must include a scheme and authority: ${request.url}',
      request.url,
    );
  }

  final byteStream = request.finalize();

  final requestFields = _requestHeadersToFields(
    _types,
    request.url,
    request.headers,
  );

  final Option<Stream<Uint8List>> contents;
  if (request.contentLength == 0) {
    contents = Option.none;
  } else {
    final bodyStream = byteStream.map(
      (chunk) => chunk is Uint8List ? chunk : Uint8List.fromList(chunk),
    );
    contents = Option.some(bodyStream);
  }

  final (wasiRequest, transmitFuture) = _types.staticRequestNew(
    headers: requestFields,
    contents: contents,
    trailers: Future.syncValue(const Result.ok(Option.none)),
    options: Option.none,
  );
  transmitFuture.ignore();

  final reqBorrow = wasiRequest.borrow();
  if (_types.methodRequestSetMethod(
        self: reqBorrow,
        method: _toWasiMethod(request.method),
      )
      case ErrorResult()) {
    wasiRequest.drop();
    throw http.ClientException(
      'Invalid HTTP method: ${request.method}',
      request.url,
    );
  }

  if (_types.methodRequestSetScheme(
        self: reqBorrow,
        scheme: Option.some(_toWasiScheme(request.url.scheme)),
      )
      case ErrorResult()) {
    wasiRequest.drop();
    throw http.ClientException(
      'Invalid URI scheme: ${request.url.scheme}',
      request.url,
    );
  }

  if (_types.methodRequestSetAuthority(
        self: reqBorrow,
        authority: Option.some(request.url.authority),
      )
      case ErrorResult()) {
    wasiRequest.drop();
    throw http.ClientException(
      'Invalid URI authority: ${request.url.authority}',
      request.url,
    );
  }

  final path = request.url.path.isEmpty ? '/' : request.url.path;
  final pathWithQuery = request.url.hasQuery
      ? '$path?${request.url.query}'
      : path;
  if (_types.methodRequestSetPathWithQuery(
        self: reqBorrow,
        pathWithQuery: Option.some(pathWithQuery),
      )
      case ErrorResult()) {
    wasiRequest.drop();
    throw http.ClientException(
      'Invalid URI path/query: $pathWithQuery',
      request.url,
    );
  }

  final sendResult = await _client.send(request: wasiRequest);
  final Owned<TypesResponse> wasiResponse;
  switch (sendResult) {
    case OkResult(:final value):
      wasiResponse = value;
    case ErrorResult(value: final error):
      throw http.ClientException(
        'WASI HTTP error (${_describeErrorCode(error)})',
        request.url,
      );
  }

  final respBorrow = wasiResponse.borrow();
  final statusCode = _types.methodResponseGetStatusCode(self: respBorrow);

  final rawHeadersOwned = _types.methodResponseGetHeaders(self: respBorrow);
  final rawHeaderPairs = _types.methodFieldsCopyAll(
    self: rawHeadersOwned.borrow(),
  );
  rawHeadersOwned.drop();

  final responseHeaders = <String, String>{};
  for (final (name, bytes) in rawHeaderPairs) {
    final key = name.toLowerCase();
    final value = utf8.decode(bytes, allowMalformed: true);
    final existing = responseHeaders[key];
    responseHeaders[key] = existing == null ? value : '$existing, $value';
  }

  final responseResCompleter = Completer<Result<void, TypesErrorCode>>();
  final (rawBodyStream, responseTrailers) = _types.staticResponseConsumeBody(
    $this: wasiResponse,
    res: responseResCompleter.future,
  );

  void finishResponse() {
    if (!responseResCompleter.isCompleted) {
      responseResCompleter.complete(const Result.ok(null));
    }
  }

  final responseBodyStream = _trackStreamCompletion(
    rawBodyStream,
    responseTrailers,
    request.url,
    finishResponse,
  );

  final contentLengthHeader = responseHeaders['content-length'];
  final contentLength = contentLengthHeader != null
      ? int.tryParse(contentLengthHeader)
      : null;

  return http.StreamedResponse(
    responseBodyStream,
    statusCode,
    contentLength: contentLength,
    request: request,
    headers: responseHeaders,
  );
}