Skip to content

feat: support multiple headers #1090

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ analyzer:

linter:
rules:
- avoid_bool_literals_in_conditional_expressions
- avoid_classes_with_only_static_members
- avoid_private_typedef_functions
- avoid_returning_this
- avoid_unused_constructor_parameters
- cascade_invocations
- join_return_with_assignment
- missing_whitespace_between_adjacent_strings
- no_adjacent_strings_in_list
- no_runtimeType_toString
- prefer_const_declarations
- prefer_expression_function_bodies
- use_string_buffers
- avoid_bool_literals_in_conditional_expressions
- avoid_classes_with_only_static_members
- avoid_private_typedef_functions
- avoid_returning_this
- avoid_unused_constructor_parameters
- cascade_invocations
- join_return_with_assignment
- missing_whitespace_between_adjacent_strings
- no_adjacent_strings_in_list
- no_runtimeType_toString
- prefer_const_declarations
- prefer_expression_function_bodies
- use_string_buffers
26 changes: 18 additions & 8 deletions pkgs/http/lib/http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export 'src/base_response.dart';
export 'src/byte_stream.dart';
export 'src/client.dart' hide zoneClient;
export 'src/exception.dart';
export 'src/headers.dart';
export 'src/multipart_file.dart';
export 'src/multipart_request.dart';
export 'src/request.dart';
Expand All @@ -34,7 +35,7 @@ export 'src/streamed_response.dart';
/// the same server, you should use a single [Client] for all of those requests.
///
/// For more fine-grained control over the request, use [Request] instead.
Future<Response> head(Uri url, {Map<String, String>? headers}) =>
Future<Response> head(Uri url, {Map<String, Iterable<String>>? headers}) =>
_withClient((client) => client.head(url, headers: headers));

/// Sends an HTTP GET request with the given headers to the given URL.
Expand All @@ -44,7 +45,7 @@ Future<Response> head(Uri url, {Map<String, String>? headers}) =>
/// the same server, you should use a single [Client] for all of those requests.
///
/// For more fine-grained control over the request, use [Request] instead.
Future<Response> get(Uri url, {Map<String, String>? headers}) =>
Future<Response> get(Uri url, {Map<String, Iterable<String>>? headers}) =>
_withClient((client) => client.get(url, headers: headers));

/// Sends an HTTP POST request with the given headers and body to the given URL.
Expand All @@ -66,7 +67,9 @@ Future<Response> get(Uri url, {Map<String, String>? headers}) =>
/// For more fine-grained control over the request, use [Request] or
/// [StreamedRequest] instead.
Future<Response> post(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
{Map<String, Iterable<String>>? headers,
Object? body,
Encoding? encoding}) =>
_withClient((client) =>
client.post(url, headers: headers, body: body, encoding: encoding));

Expand All @@ -89,7 +92,9 @@ Future<Response> post(Uri url,
/// For more fine-grained control over the request, use [Request] or
/// [StreamedRequest] instead.
Future<Response> put(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
{Map<String, Iterable<String>>? headers,
Object? body,
Encoding? encoding}) =>
_withClient((client) =>
client.put(url, headers: headers, body: body, encoding: encoding));

Expand All @@ -113,7 +118,9 @@ Future<Response> put(Uri url,
/// For more fine-grained control over the request, use [Request] or
/// [StreamedRequest] instead.
Future<Response> patch(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
{Map<String, Iterable<String>>? headers,
Object? body,
Encoding? encoding}) =>
_withClient((client) =>
client.patch(url, headers: headers, body: body, encoding: encoding));

Expand All @@ -125,7 +132,9 @@ Future<Response> patch(Uri url,
///
/// For more fine-grained control over the request, use [Request] instead.
Future<Response> delete(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
{Map<String, Iterable<String>>? headers,
Object? body,
Encoding? encoding}) =>
_withClient((client) =>
client.delete(url, headers: headers, body: body, encoding: encoding));

Expand All @@ -141,7 +150,7 @@ Future<Response> delete(Uri url,
///
/// For more fine-grained control over the request and response, use [Request]
/// instead.
Future<String> read(Uri url, {Map<String, String>? headers}) =>
Future<String> read(Uri url, {Map<String, Iterable<String>>? headers}) =>
_withClient((client) => client.read(url, headers: headers));

/// Sends an HTTP GET request with the given headers to the given URL and
Expand All @@ -157,7 +166,8 @@ Future<String> read(Uri url, {Map<String, String>? headers}) =>
///
/// For more fine-grained control over the request and response, use [Request]
/// instead.
Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers}) =>
Future<Uint8List> readBytes(Uri url,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition, similar to here, I think it is completely possible to use Object? to set headers, because headers are made according to MDN standards, so in theory it can allow many structures to be converted into headers

{Map<String, Iterable<String>>? headers}) =>
_withClient((client) => client.readBytes(url, headers: headers));

Future<T> _withClient<T>(Future<T> Function(Client) fn) async {
Expand Down
8 changes: 7 additions & 1 deletion pkgs/http/lib/retry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,16 @@ final class RetryClient extends BaseClient {
final request = StreamedRequest(original.method, original.url)
..contentLength = original.contentLength
..followRedirects = original.followRedirects
..headers.addAll(original.headers)
..maxRedirects = original.maxRedirects
..persistentConnection = original.persistentConnection;

for (final (name, value) in original.headers.entries()) {
request.headers.append(name, value);
}
for (final cookie in original.headers.getSetCookie()) {
request.headers.append('set-cookie', cookie);
}

body.listen(request.sink.add,
onError: request.sink.addError,
onDone: request.sink.close,
Expand Down
39 changes: 29 additions & 10 deletions pkgs/http/lib/src/base_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'base_request.dart';
import 'byte_stream.dart';
import 'client.dart';
import 'exception.dart';
import 'headers.dart';
import 'request.dart';
import 'response.dart';
import 'streamed_response.dart';
Expand All @@ -19,42 +20,51 @@ import 'streamed_response.dart';
/// maybe [close], and then they get various convenience methods for free.
abstract mixin class BaseClient implements Client {
@override
Future<Response> head(Uri url, {Map<String, String>? headers}) =>
Future<Response> head(Uri url, {Map<String, Iterable<String>>? headers}) =>
_sendUnstreamed('HEAD', url, headers);

@override
Future<Response> get(Uri url, {Map<String, String>? headers}) =>
Future<Response> get(Uri url, {Map<String, Iterable<String>>? headers}) =>
_sendUnstreamed('GET', url, headers);

@override
Future<Response> post(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
{Map<String, Iterable<String>>? headers,
Object? body,
Encoding? encoding}) =>
_sendUnstreamed('POST', url, headers, body, encoding);

@override
Future<Response> put(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
{Map<String, Iterable<String>>? headers,
Object? body,
Encoding? encoding}) =>
_sendUnstreamed('PUT', url, headers, body, encoding);

@override
Future<Response> patch(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
{Map<String, Iterable<String>>? headers,
Object? body,
Encoding? encoding}) =>
_sendUnstreamed('PATCH', url, headers, body, encoding);

@override
Future<Response> delete(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) =>
{Map<String, Iterable<String>>? headers,
Object? body,
Encoding? encoding}) =>
_sendUnstreamed('DELETE', url, headers, body, encoding);

@override
Future<String> read(Uri url, {Map<String, String>? headers}) async {
Future<String> read(Uri url, {Map<String, Iterable<String>>? headers}) async {
final response = await get(url, headers: headers);
_checkResponseSuccess(url, response);
return response.body;
}

@override
Future<Uint8List> readBytes(Uri url, {Map<String, String>? headers}) async {
Future<Uint8List> readBytes(Uri url,
{Map<String, Iterable<String>>? headers}) async {
final response = await get(url, headers: headers);
_checkResponseSuccess(url, response);
return response.bodyBytes;
Expand All @@ -72,11 +82,20 @@ abstract mixin class BaseClient implements Client {

/// Sends a non-streaming [Request] and returns a non-streaming [Response].
Future<Response> _sendUnstreamed(
String method, Uri url, Map<String, String>? headers,
String method, Uri url, Map<String, Iterable<String>>? headers,
[Object? body, Encoding? encoding]) async {
var request = Request(method, url);

if (headers != null) request.headers.addAll(headers);
if (headers != null) {
final newHeaders = Headers(headers);
for (final (name, value) in newHeaders.entries()) {
request.headers.append(name, value);
}
for (final cookie in newHeaders.getSetCookie()) {
request.headers.append('Set-Cookie', cookie);
}
}

if (encoding != null) request.encoding = encoding;
if (body != null) {
if (body is String) {
Expand Down
11 changes: 3 additions & 8 deletions pkgs/http/lib/src/base_request.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:collection';

import 'package:meta/meta.dart';

import '../http.dart' show ClientException, get;
import 'base_client.dart';
import 'base_response.dart';
import 'byte_stream.dart';
import 'client.dart';
import 'headers.dart';
import 'streamed_response.dart';
import 'utils.dart';

Expand Down Expand Up @@ -82,7 +81,7 @@ abstract class BaseRequest {
// TODO(nweiz): automatically parse cookies from headers

// TODO(nweiz): make this a HttpHeaders object
final Map<String, String> headers;
final Headers headers = Headers();

/// Whether [finalize] has been called.
bool get finalized => _finalized;
Expand All @@ -96,11 +95,7 @@ abstract class BaseRequest {
return method;
}

BaseRequest(String method, this.url)
: method = _validateMethod(method),
headers = LinkedHashMap(
equals: (key1, key2) => key1.toLowerCase() == key2.toLowerCase(),
hashCode: (key) => key.toLowerCase().hashCode);
BaseRequest(String method, this.url) : method = _validateMethod(method);

/// Finalizes the HTTP request in preparation for it being sent.
///
Expand Down
8 changes: 5 additions & 3 deletions pkgs/http/lib/src/base_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'base_client.dart';
import 'base_request.dart';
import 'headers.dart';

/// The base class for HTTP responses.
///
Expand Down Expand Up @@ -47,7 +48,7 @@ abstract class BaseResponse {
/// by a single space. Leading and trailing whitespace in header values are
/// always removed.
// TODO(nweiz): make this a HttpHeaders object.
final Map<String, String> headers;
final Headers headers;

final bool isRedirect;

Expand All @@ -57,10 +58,11 @@ abstract class BaseResponse {
BaseResponse(this.statusCode,
{this.contentLength,
this.request,
this.headers = const {},
Headers? headers,
this.isRedirect = false,
this.persistentConnection = true,
this.reasonPhrase}) {
this.reasonPhrase})
: headers = headers ?? Headers() {
if (statusCode < 100) {
throw ArgumentError('Invalid status code $statusCode.');
} else if (contentLength != null && contentLength! < 0) {
Expand Down
37 changes: 23 additions & 14 deletions pkgs/http/lib/src/browser_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:js_interop';

import 'package:web/helpers.dart';
import 'package:web/helpers.dart' hide Headers;

import 'base_client.dart';
import 'base_request.dart';
import 'byte_stream.dart';
import 'exception.dart';
import 'headers.dart';
import 'streamed_response.dart';

final _digitRegex = RegExp(r'^\d+$');
Expand Down Expand Up @@ -62,14 +64,22 @@ class BrowserClient extends BaseClient {
..open(request.method, '${request.url}', true)
..responseType = 'arraybuffer'
..withCredentials = withCredentials;
for (var header in request.headers.entries) {
xhr.setRequestHeader(header.key, header.value);

// Sets all headers without set-cookie headers.
for (final (name, value) in request.headers.entries()) {
xhr.setRequestHeader(name, value);
}

// Sets cookie headers.
for (final cookie in request.headers.getSetCookie()) {
xhr.setRequestHeader('set-cookie', cookie);
}

var completer = Completer<StreamedResponse>();

unawaited(xhr.onLoad.first.then((_) {
if (xhr.responseHeaders['content-length'] case final contentLengthHeader
if (xhr.responseHeaders.get('content-length')
case final contentLengthHeader
when contentLengthHeader != null &&
!_digitRegex.hasMatch(contentLengthHeader)) {
completer.completeError(ClientException(
Expand Down Expand Up @@ -118,12 +128,12 @@ class BrowserClient extends BaseClient {
}

extension on XMLHttpRequest {
Map<String, String> get responseHeaders {
Headers get responseHeaders {
final headers = Headers();
final lines = const LineSplitter().convert(getAllResponseHeaders());

// from Closure's goog.net.Xhrio.getResponseHeaders.
var headers = <String, String>{};
var headersString = getAllResponseHeaders();
var headersList = headersString.split('\r\n');
for (var header in headersList) {
for (var header in lines) {
if (header.isEmpty) {
continue;
}
Expand All @@ -132,14 +142,13 @@ extension on XMLHttpRequest {
if (splitIdx == -1) {
continue;
}

var key = header.substring(0, splitIdx).toLowerCase();
var value = header.substring(splitIdx + 2);
if (headers.containsKey(key)) {
headers[key] = '${headers[key]}, $value';
} else {
headers[key] = value;
}

headers.append(key, value);
}
// return headers;
return headers;
}
}
Loading