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 urlRequest = MutableURLRequest.fromUrl(request.url)
..httpMethod = request.method;
if (request is Request) {
// Optimize the (typical) `Request` case since assigning to
// `httpBodyStream` requires a lot of expensive setup and data passing.
urlRequest.httpBody = Data.fromList(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.
urlRequest.httpBodyStream = s;
}
// This will preserve Apple default headers - is that what we want?
request.headers.forEach(urlRequest.setValueForHttpHeaderField);
final task = urlSession.dataTaskWithRequest(urlRequest);
final taskTracker = _TaskTracker(request);
_tasks[task] = taskTracker;
task.resume();
final maxRedirects = request.followRedirects ? request.maxRedirects : 0;
final 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,
);
}
return _StreamedResponseWithUrl(
taskTracker.responseController.stream,
response.statusCode,
url: taskTracker.lastUrl ?? request.url,
contentLength: response.expectedContentLength == -1
? null
: response.expectedContentLength,
reasonPhrase: _findReasonPhrase(response.statusCode),
request: request,
isRedirect: !request.followRedirects && taskTracker.numRedirects > 0,
headers: responseHeaders,
);
}