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 ClientException
s.
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}');
}
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) {
urlRequest.httpBodyStream = s.toNSInputStream();
} else {
final splitter = StreamSplitter(s);
urlRequest.httpBodyStream = splitter.split().toNSInputStream();
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);
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;
result = await taskTracker.responseCompleter.future;
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,
);
}