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<StreamedResponse> send(BaseRequest request) async {
  // The expected success case flow (without redirects) is:
  // 1. send is called by BaseClient
  // 2. send starts the request with UrlSession.dataTaskWithRequest and waits
  //    on a Completer
  // 3. _onResponse is called with the HTTP headers, status code, etc.
  // 4. _onResponse calls complete on the Completer that send is waiting on.
  // 5. send continues executing and returns a StreamedResponse.
  //    StreamedResponse contains a Stream<UInt8List>.
  // 6. _onData is called one or more times and adds that to the
  //    StreamController that controls the Stream<UInt8List>
  // 7. _onComplete is called after all the data is read and closes the
  //    StreamController
  if (_urlSession == null) {
    throw ClientException(
      'HTTP request failed. Client is already closed.',
      request.url,
    );
  }
  final urlSession = _urlSession!;

  final stream = request.finalize();

  final profile = _createProfile(request);
  profile?.connectionInfo = {
    'package': 'package:cupertino_http',
    'client': 'CupertinoClient',
    'configuration': _urlSession!.configuration.toString(),
  };
  profile?.requestData
    ?..contentLength = request.contentLength
    ..followRedirects = request.followRedirects
    ..headersCommaValues = request.headers
    ..maxRedirects = request.maxRedirects;

  final urlRequest = MutableURLRequest.fromUrl(request.url)
    ..httpMethod = request.method;

  if (request.contentLength != null) {
    profile?.requestData.headersListValues = {
      'Content-Length': ['${request.contentLength}'],
      ...profile.requestData.headers!,
    };
    urlRequest.setValueForHttpHeaderField(
      'Content-Length',
      '${request.contentLength}',
    );
  }

  NSInputStream? nsStream;
  if (request is Request) {
    // Optimize the (typical) `Request` case since assigning to
    // `httpBodyStream` requires a lot of expensive setup and data passing.
    urlRequest.httpBody = request.bodyBytes.toNSData();
    profile?.requestData.bodySink.add(request.bodyBytes);
  } else if (await _hasData(stream) case (true, final s)) {
    // If the request is supposed to be bodyless (e.g. GET requests)
    // then setting `httpBodyStream` will cause the request to fail -
    // even if the stream is empty.
    if (profile == null) {
      nsStream = s.toNSInputStream();
      urlRequest.httpBodyStream = nsStream;
    } else {
      final splitter = StreamSplitter(s);
      nsStream = splitter.split().toNSInputStream();
      urlRequest.httpBodyStream = nsStream;
      unawaited(profile.requestData.bodySink.addStream(splitter.split()));
    }
  }

  // This will preserve Apple default headers - is that what we want?
  request.headers.forEach(urlRequest.setValueForHttpHeaderField);
  final task = urlSession.dataTaskWithRequest(urlRequest);
  if (request case Abortable(:final abortTrigger?)) {
    unawaited(
      abortTrigger.whenComplete(() {
        final taskTracker = _tasks[task];
        if (taskTracker == null) return;
        taskTracker.requestAborted = true;
        task.cancel();
      }),
    );
  }

  final subscription = StreamController<Uint8List>(
    onCancel: () {
      final taskTracker = _tasks[task];
      if (taskTracker == null) return;
      taskTracker.responseListenerCancelled = true;
      task.cancel();
    },
  );
  final taskTracker = _TaskTracker(request, subscription, profile);
  _tasks[task] = taskTracker;
  task.resume();

  final maxRedirects = request.followRedirects ? request.maxRedirects : 0;

  late URLResponse result;
  try {
    result = await taskTracker.responseCompleter.future;
  } finally {
    // If the request is aborted before the `NSUrlSessionTask` opens the
    // `NSInputStream` attached to `NSMutableURLRequest.HTTPBodyStream`, then
    // the task will not close the `NSInputStream`.
    //
    // This will cause the Dart portion of the `NSInputStream` implementation
    // to hang waiting for a close message.
    //
    // See https://github.com/dart-lang/native/issues/2333
    if (nsStream?.streamStatus != NSStreamStatus.NSStreamStatusClosed) {
      nsStream?.close();
    }
  }

  final response = result as HTTPURLResponse;

  if (request.followRedirects && taskTracker.numRedirects > maxRedirects) {
    throw ClientException('Redirect limit exceeded', request.url);
  }

  final responseHeaders = response.allHeaderFields.map(
    (key, value) => MapEntry(key.toLowerCase(), value),
  );

  if (responseHeaders['content-length'] case final contentLengthHeader?
      when !_digitRegex.hasMatch(contentLengthHeader)) {
    throw ClientException(
      'Invalid content-length header [$contentLengthHeader].',
      request.url,
    );
  }

  final contentLength = response.expectedContentLength == -1
      ? null
      : response.expectedContentLength;
  final isRedirect = !request.followRedirects && taskTracker.numRedirects > 0;
  profile?.responseData
    ?..contentLength = contentLength
    ..headersCommaValues = responseHeaders
    ..isRedirect = isRedirect
    ..reasonPhrase = _findReasonPhrase(response.statusCode)
    ..startTime = DateTime.now()
    ..statusCode = response.statusCode;

  return _StreamedResponseWithUrl(
    taskTracker.responseController.stream,
    response.statusCode,
    url: taskTracker.lastUrl ?? request.url,
    contentLength: contentLength,
    reasonPhrase: _findReasonPhrase(response.statusCode),
    request: request,
    isRedirect: isRedirect,
    headers: responseHeaders,
  );
}