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<FetchResponse> send(BaseRequest request) async {
if (_closed)
throw ClientException('Client is closed', request.url);
final requestMethod = request.method.toUpperCase();
final byteStream = request.finalize();
final RequestBody? body;
final int bodySize;
if (['GET', 'HEAD'].contains(requestMethod)) {
body = null;
bodySize = 0;
} else if (streamRequests) {
body = RequestBody.fromReadableStream(
ReadableStream(
ReadableStreamSource.fromStream(byteStream.cast<JSNumber>()),
),
);
bodySize = -1;
} else {
final bytes = await byteStream.toBytes();
body = bytes.isEmpty
? null
: RequestBody.fromTypedData(bytes);
bodySize = bytes.lengthInBytes;
}
final abortController = AbortController();
final fetchRequest = request is! FetchRequest ? null : request;
final init = FetchOptions(
body: body,
method: request.method,
redirect: (
request.followRedirects ||
(fetchRequest?.redirectPolicy ?? redirectPolicy) == RedirectPolicy.alwaysFollow
)
? RequestRedirect.follow
: RequestRedirect.manual,
headers: Headers.fromMap(request.headers),
mode: fetchRequest?.mode ?? mode,
credentials: fetchRequest?.credentials ?? credentials,
cache: fetchRequest?.cache ?? cache,
referrer: fetchRequest?.referrer ?? referrer,
referrerPolicy: fetchRequest?.referrerPolicy ?? referrerPolicy,
integrity: fetchRequest?.integrity ?? '',
keepalive: bodySize < 63 * 1024 && !streamRequests && request.persistentConnection,
signal: abortController.signal,
duplex: !streamRequests ? null : RequestDuplex.half,
);
final Response response;
try {
response = await _abortOnCloseSafeGuard(
() => fetch(request.url.toString(), init),
abortController,
);
if (
response.type == 'opaqueredirect' &&
!request.followRedirects &&
redirectPolicy != RedirectPolicy.alwaysFollow
)
return _probeRedirect(
request: request,
initialResponse: response,
init: init,
abortController: abortController,
);
} catch (e) {
throw ClientException('Failed to execute fetch: $e', request.url);
}
if (response.status == 0)
throw ClientException(
'Fetch response status code 0',
request.url,
);
if (response.body == null && requestMethod != 'HEAD')
throw StateError('Invalid state: missing body with non-HEAD request.');
final reader = response.body?.getReader();
late final void Function() abort;
abort = () {
_abortCallbacks.remove(abort);
reader?.cancel();
abortController.abort();
};
_abortCallbacks.add(abort);
final int? contentLength;
final int? expectedBodyLength;
if (response.headers.get('Content-Length') case final value?) {
contentLength = int.tryParse(value);
if (contentLength == null || contentLength < 0)
throw ClientException('Content-Length header must be a positive integer value.', request.url);
// Although `identity` SHOULD NOT be used in the Content-Encoding
// according to [RFC 2616](https://www.rfc-editor.org/rfc/rfc2616#section-3.5),
// we'll handle this edge case anyway.
final encoding = response.headers.get('Content-Encoding');
if (response.responseType == ResponseType.cors) {
// For cors response we should ensure that we actually have access to
// Content-Encoding header, otherwise response can be encoded but
// we won't be able to detect it.
final exposedHeaders = response.headers.get('Access-Control-Expose-Headers')?.toLowerCase();
if (exposedHeaders != null && (
exposedHeaders.contains('*') ||
exposedHeaders.contains('content-encoding')
) && (
encoding == null ||
encoding.toLowerCase() == 'identity'
))
expectedBodyLength = contentLength;
else
expectedBodyLength = null;
} else {
// In non-cors response we have access to Content-Encoding header
if (encoding == null || encoding.toLowerCase() == 'identity')
expectedBodyLength = contentLength;
else
expectedBodyLength = null;
}
} else {
contentLength = null;
expectedBodyLength = null;
}
final stream = onDone(
reader == null
? const Stream<Uint8List>.empty()
: _readAsStream(
reader: reader,
expectedLength: expectedBodyLength,
uri: request.url,
),
abort,
);
return FetchResponse(
stream,
response.status,
cancel: abort,
url: Uri.parse(response.url),
redirected: response.redirected,
request: request,
headers: {
for (final (name, value) in response.headers.entries())
name: value,
},
isRedirect: false,
persistentConnection: false,
reasonPhrase: response.statusText,
contentLength: contentLength,
);
}