send method

  1. @override
Future<FetchResponse> 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<FetchResponse> send(BaseRequest request) async {
  if (_closed)
    throw ClientException('Client is closed', request.url);
  final requestMethod = request.method.toUpperCase();
  final byteStream = request.finalize();
  final RequestBody? body;
  final int bodySize;
  if (['GET', 'HEAD'].contains(requestMethod)) {
    body = null;
    bodySize = 0;
  } else if (streamRequests) {
    body = RequestBody.fromReadableStream(
      ReadableStream(
        ReadableStreamSource.fromStream(byteStream.cast<JSNumber>()),
      ),
    );
    bodySize = -1;
  } else {
    final bytes = await byteStream.toBytes();
    body = bytes.isEmpty
      ? null
      : RequestBody.fromTypedData(bytes);
    bodySize = bytes.lengthInBytes;
  }

  final abortController = AbortController();

  final fetchRequest = request is! FetchRequest ? null : request;
  final init = FetchOptions(
    body: body,
    method: request.method,
    redirect: (
      request.followRedirects ||
      (fetchRequest?.redirectPolicy ?? redirectPolicy) == RedirectPolicy.alwaysFollow
    )
      ? RequestRedirect.follow
      : RequestRedirect.manual,
    headers: Headers.fromMap(request.headers),
    mode: fetchRequest?.mode ?? mode,
    credentials: fetchRequest?.credentials ?? credentials,
    cache: fetchRequest?.cache ?? cache,
    referrer: fetchRequest?.referrer ?? referrer,
    referrerPolicy: fetchRequest?.referrerPolicy ?? referrerPolicy,
    integrity: fetchRequest?.integrity ?? '',
    keepalive: bodySize < 63 * 1024 && !streamRequests && request.persistentConnection,
    signal: abortController.signal,
    duplex: !streamRequests ? null : RequestDuplex.half,
  );

  final Response response;
  try {
    response = await _abortOnCloseSafeGuard(
      () => fetch(request.url.toString(), init),
      abortController,
    );

    if (
      response.type == 'opaqueredirect' &&
      !request.followRedirects &&
      redirectPolicy != RedirectPolicy.alwaysFollow
    )
      return _probeRedirect(
        request: request,
        initialResponse: response,
        init: init,
        abortController: abortController,
      );
  } catch (e) {
    throw ClientException('Failed to execute fetch: $e', request.url);
  }

  if (response.status == 0)
    throw ClientException(
      'Fetch response status code 0',
      request.url,
    );

  if (response.body == null && requestMethod != 'HEAD')
    throw StateError('Invalid state: missing body with non-HEAD request.');

  final reader = response.body?.getReader();

  late final void Function() abort;
  abort = () {
    _abortCallbacks.remove(abort);
    reader?.cancel();
    abortController.abort();
  };
  _abortCallbacks.add(abort);

  final int? contentLength;
  final int? expectedBodyLength;
  if (response.headers.get('Content-Length') case final value?) {
    contentLength = int.tryParse(value);
    if (contentLength == null || contentLength < 0)
      throw ClientException('Content-Length header must be a positive integer value.', request.url);

    // Although `identity` SHOULD NOT be used in the Content-Encoding
    // according to [RFC 2616](https://www.rfc-editor.org/rfc/rfc2616#section-3.5),
    // we'll handle this edge case anyway.
    final encoding = response.headers.get('Content-Encoding');
    if (response.responseType == ResponseType.cors) {
      // For cors response we should ensure that we actually have access to
      // Content-Encoding header, otherwise response can be encoded but
      // we won't be able to detect it.
      final exposedHeaders = response.headers.get('Access-Control-Expose-Headers')?.toLowerCase();
      if (exposedHeaders != null && (
        exposedHeaders.contains('*') ||
        exposedHeaders.contains('content-encoding')
      ) && (
        encoding == null ||
        encoding.toLowerCase() == 'identity'
      ))
        expectedBodyLength = contentLength;
      else
        expectedBodyLength = null;
    } else {
      // In non-cors response we have access to Content-Encoding header
      if (encoding == null || encoding.toLowerCase() == 'identity')
        expectedBodyLength = contentLength;
      else
        expectedBodyLength = null;
    }
  } else {
    contentLength = null;
    expectedBodyLength = null;
  }

  final stream = onDone(
    reader == null
      ? const Stream<Uint8List>.empty()
      : _readAsStream(
        reader: reader,
        expectedLength: expectedBodyLength,
        uri: request.url,
      ),
    abort,
  );

  return FetchResponse(
    stream,
    response.status,
    cancel: abort,
    url: Uri.parse(response.url),
    redirected: response.redirected,
    request: request,
    headers: {
      for (final (name, value) in response.headers.entries())
        name: value,
    },
    isRedirect: false,
    persistentConnection: false,
    reasonPhrase: response.statusText,
    contentLength: contentLength,
  );
}