send method

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

Sends an HTTP request and asynchronously returns the response.

Implementation

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

  var bytes = await request.finalize().toBytes();

  var xhr = HttpRequest();

  // Life-cycle tracking is implemented using three completers and the
  // onReadyStateChange event. The three completers are:
  //
  // - connectCompleter (completes when OPENED) (initiates sendingCompleter)
  // - sendingCompleter (completes when HEADERS_RECEIVED)
  // (initiates receivingCompleter)
  // - receivingCompleter (completes when DONE)
  //
  // connectCompleter is initiated immediately and on completion initiates
  // sendingCompleter, and so on.
  //
  // Note 'initiated' is not 'initialized' - initiated refers to a timeout
  // being set on the completer, to ensure the step completes within the
  // specified timeout.
  final controller = request.controller;

  if (controller != null) {
    if (controller.hasLifecycleTimeouts) {
      // The browser client (which uses XHR) seems not to be able to work with
      // partial (streamed) requests or responses, so the receive timeout is
      // handled by the browser client itself.
      final tracker = controller.track(request, isStreaming: false);

      // Returns a completer for the given state if a timeout is specified
      // for it, otherwise returns null.
      Completer<void>? completer(RequestLifecycleState state) =>
          controller.hasTimeoutForLifecycleState(state)
              ? Completer<void>()
              : null;

      final connectCompleter = completer(RequestLifecycleState.connecting);
      final sendingCompleter = completer(RequestLifecycleState.sending);
      final receivingCompleter = completer(RequestLifecycleState.receiving);

      // Simply abort the XHR if a timeout or cancellation occurs.
      void handleCancel(_) => xhr.abort();

      // If a connect timeout is specified, initiate the connectCompleter.
      if (connectCompleter != null) {
        unawaited(tracker.trackRequestState(
          connectCompleter.future,
          state: RequestLifecycleState.connecting,
          onCancel: handleCancel,
        ));
      }

      xhr.onReadyStateChange.listen((_) {
        // If the connection is at the OPENED stage and the
        // connectCompleter has not yet been marked as completed, complete it.
        if (xhr.readyState == HttpRequest.OPENED) {
          if (connectCompleter != null) {
            connectCompleter.complete();
          }

          // Initiate the sendingCompleter if there is a timeout specified for
          // it.
          if (sendingCompleter != null) {
            unawaited(tracker.trackRequestState(
              sendingCompleter.future,
              state: RequestLifecycleState.sending,
              onCancel: handleCancel,
            ));
          }
        }

        // If the connection is at the HEADERS_RECEIVED stage and
        // the sendingCompleter has not yet been marked as completed,
        // complete it.
        if (xhr.readyState == HttpRequest.HEADERS_RECEIVED) {
          if (sendingCompleter != null) {
            sendingCompleter.complete();
          }

          // Initiate the receivingCompleter if there is a timeout specified
          // for it.
          if (receivingCompleter != null) {
            unawaited(tracker.trackRequestState(
              receivingCompleter.future,
              state: RequestLifecycleState.receiving,
              onCancel: handleCancel,
            ));
          }
        }

        // If the connection is at least at the DONE stage and the
        // receivingCompleter has not yet been marked as completed, complete
        // it.
        if (xhr.readyState == HttpRequest.DONE) {
          if (receivingCompleter != null) {
            receivingCompleter.complete();
          }
        }
      });
    }
  }

  _xhrs.add(xhr);
  xhr
    ..open(request.method, '${request.url}', async: true)
    ..responseType = 'arraybuffer'
    ..withCredentials = withCredentials;
  request.headers.forEach(xhr.setRequestHeader);

  var completer = Completer<StreamedResponse>();

  unawaited(xhr.onLoad.first.then((_) {
    if (xhr.responseHeaders['content-length'] case final contentLengthHeader?
        when !_digitRegex.hasMatch(contentLengthHeader)) {
      completer.completeError(ClientException(
        'Invalid content-length header [$contentLengthHeader].',
        request.url,
      ));
      return;
    }
    var body = (xhr.response as ByteBuffer).asUint8List();
    completer.complete(StreamedResponse(
        ByteStream.fromBytes(body), xhr.status!,
        contentLength: body.length,
        request: request,
        headers: xhr.responseHeaders,
        reasonPhrase: xhr.statusText));
  }));

  unawaited(xhr.onError.first.then((_) {
    // Unfortunately, the underlying XMLHttpRequest API doesn't expose any
    // specific information about the error itself.
    completer.completeError(
        ClientException('XMLHttpRequest error.', request.url),
        StackTrace.current);
  }));

  xhr.send(bytes);

  try {
    return await completer.future;
  } finally {
    _xhrs.remove(xhr);
  }
}