send method

  1. @override
Future<HttpResponse> send(
  1. HttpRequest request,
  2. CancellationToken parent
)
override

Implementation

@override
Future<HttpResponse> send(
    HttpRequest request, CancellationToken parent) async {
  if (_closed) throw StateError('Transport closed.');
  parent.throwIfCanceled();
  if (request.body != null &&
      {HttpMethod.get, HttpMethod.head}.contains(request.method)) {
    throw UnsupportedError('Browser Fetch cannot send GET/HEAD bodies.');
  }
  final token = CancellationToken.linked([parent]);
  final controller = BrowserAbortController();
  _active.add(token);
  HttpResponse? response;
  final remove = token.onCancel(() {
    controller.abort();
    final current = response;
    if (current != null) unawaited(current.body.abort(token.error));
  });
  void finish() {
    remove();
    token.dispose();
    _active.remove(token);
  }

  try {
    final headers = BrowserHeaders();
    request.headers.values.forEach((name, values) {
      if ({
            'host',
            'content-length',
            'connection',
            'cookie',
            'set-cookie',
            'transfer-encoding',
            'accept-encoding',
            'origin',
            'referer'
          }.contains(name) ||
          name.startsWith('sec-') ||
          name.startsWith('proxy-')) {
        throw UnsupportedError('Browser controls the "$name" header.');
      }
      for (final value in values) {
        headers.append(name, value);
      }
    });
    final cookieName = options.xsrfCookieName;
    if (cookieName != null &&
        request.uri.origin == browserOrigin &&
        !{HttpMethod.get, HttpMethod.head, HttpMethod.options}
            .contains(request.method)) {
      for (final cookie in browserCookies.split(';')) {
        final pair = cookie.trim();
        if (pair.startsWith('$cookieName=')) {
          headers.append(options.xsrfHeaderName,
              Uri.decodeComponent(pair.substring(cookieName.length + 1)));
        }
      }
    }
    JSUint8Array? payload;
    final body = request.body;
    if (body != null) {
      final length = body.contentLength;
      if (length == null || length < 0 || length > options.maxUploadBytes) {
        throw UnsupportedError(
            'Browser uploads require a known length within maxUploadBytes.');
      }
      final bytes = BytesBuilder();
      final chunks = StreamIterator(body.openRead());
      try {
        while (await token.race(chunks.moveNext())) {
          token.throwIfCanceled();
          final chunk = chunks.current;
          if (bytes.length + chunk.length > options.maxUploadBytes) {
            throw StateError('Upload limit exceeded.');
          }
          bytes.add(chunk);
        }
      } finally {
        await chunks.cancel();
      }
      if (bytes.length != length) {
        throw StateError('Upload body does not match its declared length.');
      }
      payload = bytes.takeBytes().toJS;
    }
    final incoming = await token.race(browserFetch(
            request.uri.toString(),
            BrowserRequestInit(
                method: request.method.value,
                headers: headers,
                body: payload,
                signal: controller.signal,
                credentials: options.credentials,
                redirect: options.redirect,
                cache: options.cache,
                mode: 'cors',
                referrerPolicy: options.referrerPolicy))
        .toDart);
    if (incoming.status == 0) {
      throw UnsupportedError(
          'Opaque browser responses cannot expose HTTP data.');
    }
    final values = <String, List<String>>{};
    incoming.headers
        .forEach(((JSString value, JSString name, JSAny? headers) {
      values[name.toDart] = [value.toDart];
    }).toJS);
    final reader = incoming.body?.getReader();
    response = HttpResponse(
        statusCode: incoming.status,
        uri: Uri.parse(incoming.url),
        headers: HttpHeaders(values),
        body: _read(reader, token),
        onClose: () async {
          try {
            await reader?.cancel().toDart;
          } catch (_) {
            // An already-aborted Fetch stream can reject cancel(). The body
            // carries the original cancellation/timeout; cleanup must not
            // replace it with a browser AbortError.
            if (!token.isCanceled) rethrow;
          } finally {
            reader?.releaseLock();
            finish();
          }
        });
    token.throwIfCanceled();
    return response;
  } catch (error, stack) {
    controller.abort();
    if (response != null) await response.dispose();
    finish();
    if (token.isCanceled) Error.throwWithStackTrace(token.error, stack);
    if (error is ArgumentError ||
        error is UnsupportedError ||
        error is StateError) {
      rethrow;
    }
    Error.throwWithStackTrace(
        NetworkException(request.uri, error, stack), stack);
  }
}