send method
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,
);
}