Skip to content

Commit 75be84a

Browse files
committed
Fix content type
1 parent ebd0b33 commit 75be84a

File tree

3 files changed

+141
-95
lines changed

3 files changed

+141
-95
lines changed

lib/src/body.dart

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'dart:convert';
88
import 'package:async/async.dart';
99
import 'package:collection/collection.dart';
1010

11+
import 'utils.dart';
12+
1113
/// The body of a request or response.
1214
///
1315
/// This tracks whether the body has been read. It's separate from [Message]
@@ -36,25 +38,20 @@ class Body {
3638
/// used to convert it to a [Stream<List<int>>].
3739
factory Body(body, [Encoding encoding]) {
3840
if (body is Body) return body;
41+
if (body == null)
42+
return new Body._(new Stream.fromIterable([]), encoding, 0);
3943

4044
Stream<List<int>> stream;
4145
int contentLength;
42-
if (body == null) {
43-
contentLength = 0;
44-
stream = new Stream.fromIterable([]);
45-
} else if (body is String) {
46-
if (encoding == null) {
47-
var encoded = UTF8.encode(body);
48-
// If the text is plain ASCII, don't modify the encoding. This means
49-
// that an encoding of "text/plain" will stay put.
50-
if (!_isPlainAscii(encoded, body.length)) encoding = UTF8;
51-
contentLength = encoded.length;
52-
stream = new Stream.fromIterable([encoded]);
53-
} else {
54-
var encoded = encoding.encode(body);
55-
contentLength = encoded.length;
56-
stream = new Stream.fromIterable([encoded]);
57-
}
46+
47+
if (body is Map) {
48+
body = mapToQuery(body, encoding: encoding);
49+
}
50+
51+
if (body is String) {
52+
var encoded = encoding.encode(body);
53+
contentLength = encoded.length;
54+
stream = new Stream.fromIterable([encoded]);
5855
} else if (body is List) {
5956
contentLength = body.length;
6057
stream = new Stream.fromIterable([DelegatingList.typed(body)]);
@@ -68,19 +65,6 @@ class Body {
6865
return new Body._(stream, encoding, contentLength);
6966
}
7067

71-
/// Returns whether [bytes] is plain ASCII.
72-
///
73-
/// [codeUnits] is the number of code units in the original string.
74-
static bool _isPlainAscii(List<int> bytes, int codeUnits) {
75-
// Most non-ASCII code units will produce multiple bytes and make the text
76-
// longer.
77-
if (bytes.length != codeUnits) return false;
78-
79-
// Non-ASCII code units between U+0080 and U+009F produce 8-bit characters
80-
// with the high bit set.
81-
return bytes.every((byte) => byte & 0x80 == 0);
82-
}
83-
8468
/// Returns a [Stream] representing the body.
8569
///
8670
/// Can only be called once.

lib/src/message.dart

Lines changed: 122 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import 'dart:async';
66
import 'dart:convert';
77

8-
import 'package:collection/collection.dart';
98
import 'package:http_parser/http_parser.dart';
109

1110
import 'body.dart';
@@ -45,6 +44,9 @@ abstract class Message {
4544
/// This can be read via [read] or [readAsString].
4645
final Body _body;
4746

47+
/// The parsed version of the Content-Type header in [headers].
48+
final MediaType _contentType;
49+
4850
/// Creates a new [Message].
4951
///
5052
/// [body] is the message body. It may be either a [String], a [List<int>], a
@@ -61,14 +63,23 @@ abstract class Message {
6163
{Encoding encoding,
6264
Map<String, String> headers,
6365
Map<String, Object> context})
64-
: this._(new Body(body, encoding), headers, context);
66+
: this.__(body, _determineMediaType(body, encoding, headers), headers,
67+
context);
68+
69+
Message.__(body, MediaType contentType, Map<String, String> headers,
70+
Map<String, Object> context)
71+
: this._(new Body(body, encodingForMediaType(contentType, null)),
72+
contentType, headers, context);
6573

66-
Message._(Body body, Map<String, String> headers, Map<String, Object> context)
74+
Message._(Body body, MediaType contentType, Map<String, String> headers,
75+
Map<String, Object> context)
6776
: _body = body,
68-
headers = new HttpUnmodifiableMap<String>(_adjustHeaders(headers, body),
77+
headers = new HttpUnmodifiableMap<String>(
78+
_adjustHeaders(headers, body, contentType),
6979
ignoreKeyCase: true),
7080
context =
71-
new HttpUnmodifiableMap<Object>(context, ignoreKeyCase: false);
81+
new HttpUnmodifiableMap<Object>(context, ignoreKeyCase: false),
82+
_contentType = contentType;
7283

7384
/// If `true`, the stream returned by [read] won't emit any bytes.
7485
///
@@ -84,6 +95,7 @@ abstract class Message {
8495
_contentLengthCache = int.parse(headers['content-length']);
8596
return _contentLengthCache;
8697
}
98+
8799
int _contentLengthCache;
88100

89101
/// The MIME type declared in [headers].
@@ -92,11 +104,7 @@ abstract class Message {
92104
/// the MIME type, without any Content-Type parameters.
93105
///
94106
/// If [headers] doesn't have a Content-Type header, this will be `null`.
95-
String get mimeType {
96-
var contentType = _contentType;
97-
if (contentType == null) return null;
98-
return contentType.mimeType;
99-
}
107+
String get mimeType => _contentType?.mimeType;
100108

101109
/// The encoding of the body returned by [read].
102110
///
@@ -105,23 +113,7 @@ abstract class Message {
105113
///
106114
/// If [headers] doesn't have a Content-Type header or it specifies an
107115
/// encoding that [dart:convert] doesn't support, this will be `null`.
108-
Encoding get encoding {
109-
var contentType = _contentType;
110-
if (contentType == null) return null;
111-
if (!contentType.parameters.containsKey('charset')) return null;
112-
return Encoding.getByName(contentType.parameters['charset']);
113-
}
114-
115-
/// The parsed version of the Content-Type header in [headers].
116-
///
117-
/// This is cached for efficient access.
118-
MediaType get _contentType {
119-
if (_contentTypeCache != null) return _contentTypeCache;
120-
if (!headers.containsKey('content-type')) return null;
121-
_contentTypeCache = new MediaType.parse(headers['content-type']);
122-
return _contentTypeCache;
123-
}
124-
MediaType _contentTypeCache;
116+
Encoding get encoding => _body.encoding;
125117

126118
/// Returns the message body as byte chunks.
127119
///
@@ -144,55 +136,119 @@ abstract class Message {
144136
/// changes.
145137
Message change(
146138
{Map<String, String> headers, Map<String, Object> context, body});
147-
}
148139

149-
/// Adds information about encoding to [headers].
150-
///
151-
/// Returns a new map without modifying [headers].
152-
Map<String, String> _adjustHeaders(Map<String, String> headers, Body body) {
153-
var sameEncoding = _sameEncoding(headers, body);
154-
if (sameEncoding) {
155-
if (body.contentLength == null ||
156-
getHeader(headers, 'content-length') == body.contentLength.toString()) {
157-
return headers ?? const HttpUnmodifiableMap.empty();
158-
} else if (body.contentLength == 0 &&
159-
(headers == null || headers.isEmpty)) {
160-
return const HttpUnmodifiableMap.empty();
140+
/// Determines the media type based on the [headers], [encoding] and [body].
141+
static MediaType _determineMediaType(
142+
body, Encoding encoding, Map<String, String> headers) =>
143+
_headerMediaType(headers, encoding) ?? _defaultMediaType(body, encoding);
144+
145+
static MediaType _defaultMediaType(body, Encoding encoding) {
146+
//if (body == null) return null;
147+
148+
var parameters = {'charset': encoding?.name ?? UTF8.name};
149+
150+
if (body is String) {
151+
return new MediaType('text', 'plain', parameters);
152+
} else if (body is Map) {
153+
return new MediaType('application', 'x-www-form-urlencoded', parameters);
154+
} else if (encoding != null) {
155+
return new MediaType('application', 'octet-stream', parameters);
161156
}
157+
158+
return null;
162159
}
163160

164-
var newHeaders = headers == null
165-
? new CaseInsensitiveMap<String>()
166-
: new CaseInsensitiveMap<String>.from(headers);
167-
168-
if (!sameEncoding) {
169-
if (newHeaders['content-type'] == null) {
170-
newHeaders['content-type'] =
171-
'application/octet-stream; charset=${body.encoding.name}';
172-
} else {
173-
var contentType = new MediaType.parse(newHeaders['content-type'])
174-
.change(parameters: {'charset': body.encoding.name});
175-
newHeaders['content-type'] = contentType.toString();
176-
}
161+
static MediaType _headerMediaType(
162+
Map<String, String> headers, Encoding encoding) {
163+
var contentTypeHeader = getHeader(headers, 'content-type');
164+
if (contentTypeHeader == null) return null;
165+
166+
var contentType = new MediaType.parse(contentTypeHeader);
167+
var parameters = {
168+
'charset':
169+
encoding?.name ?? contentType.parameters['charset'] ?? UTF8.name
170+
};
171+
172+
return contentType.change(parameters: parameters);
177173
}
178174

179-
if (body.contentLength != null) {
180-
var coding = newHeaders['transfer-encoding'];
181-
if (coding == null || equalsIgnoreAsciiCase(coding, 'identity')) {
182-
newHeaders['content-length'] = body.contentLength.toString();
175+
/// Adjusts the [headers] to include information from the [body].
176+
///
177+
/// Returns a new map without modifying [headers].
178+
///
179+
/// The following headers could be added or modified.
180+
/// * content-length
181+
/// * content-type
182+
static Map<String, String> _adjustHeaders(
183+
Map<String, String> headers, Body body, MediaType contentType) {
184+
var modified = <String, String>{};
185+
186+
var contentLengthHeader = _adjustContentLengthHeader(headers, body);
187+
if (contentLengthHeader.isNotEmpty) {
188+
modified['content-length'] = contentLengthHeader;
189+
}
190+
191+
var contentTypeHeader = _adjustContentTypeHeader(headers, contentType);
192+
if (contentTypeHeader.isNotEmpty) {
193+
modified['content-type'] = contentTypeHeader;
194+
}
195+
196+
if (modified.isEmpty) {
197+
return headers ?? const HttpUnmodifiableMap.empty();
183198
}
199+
200+
var newHeaders = headers == null
201+
? new CaseInsensitiveMap<String>()
202+
: new CaseInsensitiveMap<String>.from(headers);
203+
204+
newHeaders.addAll(modified);
205+
206+
return newHeaders;
184207
}
185208

186-
return newHeaders;
187-
}
209+
/// Checks the `content-length` header to see if it requires modification.
210+
///
211+
/// Returns an empty string when no modification is required, otherwise it
212+
/// returns the value to set.
213+
///
214+
/// If there is a contentLength specified within the [body] and it does not
215+
/// match what is specified in the [headers] it will be modified to the body's
216+
/// value.
217+
static String _adjustContentLengthHeader(
218+
Map<String, String> headers, Body body) {
219+
var bodyContentLength = body.contentLength ?? -1;
220+
221+
if (bodyContentLength >= 0) {
222+
var bodyContentHeader = bodyContentLength.toString();
223+
224+
if (getHeader(headers, 'content-length') != bodyContentHeader) {
225+
return bodyContentHeader;
226+
}
227+
}
228+
229+
return '';
230+
}
188231

189-
/// Returns whether [headers] declares the same encoding as [body].
190-
bool _sameEncoding(Map<String, String> headers, Body body) {
191-
if (body.encoding == null) return true;
232+
/// Checks the `content-type` header to see if it requires modification.
233+
///
234+
/// Returns an empty string when no modification is required, otherwise it
235+
/// returns the value to set.
236+
///
237+
/// If the contentType within [body] is different than the one specified in the
238+
/// [headers] then body's value will be used. The [headers] were already used
239+
/// when creating the body's contentType so this will only actually change
240+
/// things when headers did not contain a `content-type`.
241+
static String _adjustContentTypeHeader(
242+
Map<String, String> headers, MediaType contentType) {
243+
var headerContentType = getHeader(headers, 'content-type');
244+
var bodyContentType = contentType?.toString();
192245

193-
var contentType = getHeader(headers, 'content-type');
194-
if (contentType == null) return false;
246+
// Neither are set so don't modify it
247+
if ((headerContentType == null) && (bodyContentType == null)) {
248+
return '';
249+
}
195250

196-
var charset = new MediaType.parse(contentType).parameters['charset'];
197-
return Encoding.getByName(charset) == body.encoding;
251+
// The value of bodyContentType will have the overridden values so use that
252+
return headerContentType != bodyContentType ? bodyContentType : '';
253+
}
198254
}

lib/src/utils.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:convert';
77
import 'dart:typed_data';
88

99
import 'package:collection/collection.dart';
10+
import 'package:http_parser/http_parser.dart';
1011

1112
import 'http_unmodifiable_map.dart';
1213

@@ -53,6 +54,11 @@ List<String> split1(String toSplit, String pattern) {
5354
];
5455
}
5556

57+
Encoding encodingForMediaType(MediaType type, [Encoding fallback = LATIN1]) {
58+
if (type == null) return null;
59+
return encodingForCharset(type.parameters['charset'], fallback);
60+
}
61+
5662
/// Returns the [Encoding] that corresponds to [charset]. Returns [fallback] if
5763
/// [charset] is null or if no [Encoding] was found that corresponds to
5864
/// [charset].

0 commit comments

Comments
 (0)