Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(77)

Side by Side Diff: lib/src/sync_http.dart

Issue 2827083002: Created a new synchronous http client using RawSynchronousSockets. (Closed)
Patch Set: Fixed issues with tests, added huge test, updated README Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « lib/src/line_decoder.dart ('k') | lib/sync_http.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 part of sync.http;
6
7 /// A simple synchronous HTTP client.
8 ///
9 /// This is a two-step process. When a [SyncHttpClientRequest] is returned the
10 /// underlying network connection has been established, but no data has yet been
11 /// sent. The HTTP headers and body can be set on the request, and close is
12 /// called to send it to the server and get the [SyncHttpClientResponse].
13 abstract class SyncHttpClient {
14 /// Send a GET request to the provided URL.
15 static SyncHttpClientRequest getUrl(Uri uri) =>
16 new SyncHttpClientRequest._('GET', uri, false);
17
18 /// Send a POST request to the provided URL.
19 static SyncHttpClientRequest postUrl(uri) =>
20 new SyncHttpClientRequest._('POST', uri, true);
21
22 /// Send a DELETE request to the provided URL.
23 static SyncHttpClientRequest deleteUrl(uri) =>
24 new SyncHttpClientRequest._('DELETE', uri, false);
25
26 /// Send a PUT request to the provided URL.
27 static SyncHttpClientRequest putUrl(uri) =>
28 new SyncHttpClientRequest._('PUT', uri, true);
29 }
30
31 /// HTTP request for a synchronous client connection.
32 class SyncHttpClientRequest {
33 static const String _protocolVersion = '1.1';
34
35 /// The length of the request body. Is set to null when no body exists.
36 int get contentLength => hasBody ? _body.length : null;
37
38 HttpHeaders _headers;
39
40 /// The headers associated with the HTTP request.
41 HttpHeaders get headers {
42 if (_headers == null) {
43 _headers = new _SyncHttpClientRequestHeaders(this);
44 }
45 return _headers;
46 }
47
48 /// The type of HTTP request being made.
49 final String method;
50
51 /// The Uri the HTTP request will be sent to.
52 final Uri uri;
53
54 /// The default encoding for the HTTP request (UTF8).
55 final Encoding encoding = UTF8;
56
57 /// The body of the HTTP request. This can be empty if there is no body
58 /// associated with the request.
59 final BytesBuilder _body;
60
61 /// The synchronous socket used to initiate the HTTP request.
62 final RawSynchronousSocket _socket;
63
64 SyncHttpClientRequest._(this.method, Uri uri, bool body)
65 : this.uri = uri,
66 this._body = body ? new BytesBuilder() : null,
67 this._socket = RawSynchronousSocket.connectSync(uri.host, uri.port);
68
69 /// Write content into the body of the HTTP request.
70 void write(Object obj) {
71 if (hasBody) {
72 _body.add(encoding.encoder.convert(obj.toString()));
73 } else {
74 throw new StateError('write not allowed for method $method');
75 }
76 }
77
78 /// Specifies whether or not the HTTP request has a body.
79 bool get hasBody => _body != null;
80
81 /// Send the HTTP request and get the response.
82 SyncHttpClientResponse close() {
83 StringBuffer buffer = new StringBuffer();
84 buffer.write('$method ${uri.path} HTTP/$_protocolVersion\r\n');
85 headers.forEach((name, values) {
86 values.forEach((value) {
87 buffer.write('$name: $value\r\n');
88 });
89 });
90 buffer.write('\r\n');
91 if (hasBody) {
92 buffer.write(new String.fromCharCodes(_body.takeBytes()));
93 }
94 _socket.writeFromSync(buffer.toString().codeUnits);
95 return new SyncHttpClientResponse(_socket);
96 }
97 }
98
99 class _SyncHttpClientRequestHeaders implements HttpHeaders {
100 Map<String, List> _headers = <String, List<String>>{};
101
102 final SyncHttpClientRequest _request;
103 ContentType contentType;
104
105 _SyncHttpClientRequestHeaders(this._request);
106
107 @override
108 List<String> operator [](String name) {
109 switch (name) {
110 case HttpHeaders.ACCEPT_CHARSET:
111 return ['utf-8'];
112 case HttpHeaders.ACCEPT_ENCODING:
113 return ['identity'];
114 case HttpHeaders.CONNECTION:
115 return ['close'];
116 case HttpHeaders.CONTENT_LENGTH:
117 if (!_request.hasBody) {
118 return null;
119 }
120 return [contentLength.toString()];
121 case HttpHeaders.CONTENT_TYPE:
122 if (contentType == null) {
123 return null;
124 }
125 return [contentType.toString()];
126 case HttpHeaders.HOST:
127 return ['$host:$port'];
128 default:
129 var values = _headers[name];
130 if (values == null || values.isEmpty) {
131 return null;
132 }
133 return values.map((e) => e.toString()).toList(growable: false);
134 }
135 }
136
137 /// Add [value] to the list of values associated with header [name].
138 @override
139 void add(String name, Object value) {
140 switch (name) {
141 case HttpHeaders.ACCEPT_CHARSET:
142 case HttpHeaders.ACCEPT_ENCODING:
143 case HttpHeaders.CONNECTION:
144 case HttpHeaders.CONTENT_LENGTH:
145 case HttpHeaders.DATE:
146 case HttpHeaders.EXPIRES:
147 case HttpHeaders.IF_MODIFIED_SINCE:
148 case HttpHeaders.HOST:
149 throw new UnsupportedError('Unsupported or immutable property: $name');
150 case HttpHeaders.CONTENT_TYPE:
151 contentType = value;
152 break;
153 default:
154 if (_headers[name] == null) {
155 _headers[name] = [];
156 }
157 _headers[name].add(value);
158 }
159 }
160
161 /// Remove [value] from the list associated with header [name].
162 @override
163 void remove(String name, Object value) {
164 switch (name) {
165 case HttpHeaders.ACCEPT_CHARSET:
166 case HttpHeaders.ACCEPT_ENCODING:
167 case HttpHeaders.CONNECTION:
168 case HttpHeaders.CONTENT_LENGTH:
169 case HttpHeaders.DATE:
170 case HttpHeaders.EXPIRES:
171 case HttpHeaders.IF_MODIFIED_SINCE:
172 case HttpHeaders.HOST:
173 throw new UnsupportedError('Unsupported or immutable property: $name');
174 case HttpHeaders.CONTENT_TYPE:
175 if (contentType == value) {
176 contentType = null;
177 }
178 break;
179 default:
180 if (_headers[name] != null) {
181 _headers[name].remove(value);
182 if (_headers[name].isEmpty) {
183 _headers.remove(name);
184 }
185 }
186 }
187 }
188
189 /// Remove all headers associated with key [name].
190 @override
191 void removeAll(String name) {
192 switch (name) {
193 case HttpHeaders.ACCEPT_CHARSET:
194 case HttpHeaders.ACCEPT_ENCODING:
195 case HttpHeaders.CONNECTION:
196 case HttpHeaders.CONTENT_LENGTH:
197 case HttpHeaders.DATE:
198 case HttpHeaders.EXPIRES:
199 case HttpHeaders.IF_MODIFIED_SINCE:
200 case HttpHeaders.HOST:
201 throw new UnsupportedError('Unsupported or immutable property: $name');
202 case HttpHeaders.CONTENT_TYPE:
203 contentType = null;
204 break;
205 default:
206 _headers.remove(name);
207 }
208 }
209
210 /// Replace values associated with key [name] with [value].
211 @override
212 void set(String name, Object value) {
213 removeAll(name);
214 add(name, value);
215 }
216
217 /// Returns the values associated with key [name], if it exists, otherwise
218 /// returns null.
219 @override
220 String value(String name) {
221 var val = this[name];
222 if (val == null || val.isEmpty) {
223 return null;
224 } else if (val.length == 1) {
225 return val[0];
226 } else {
227 throw new HttpException('header $name has more than one value');
228 }
229 }
230
231 /// Iterates over all header key-value pairs and applies [f].
232 @override
233 void forEach(void f(String name, List<String> values)) {
234 var forEachFunc = (String name) {
235 var values = this[name];
236 if (values != null && values.isNotEmpty) {
237 f(name, values);
238 }
239 };
240
241 [
242 HttpHeaders.ACCEPT_CHARSET,
243 HttpHeaders.ACCEPT_ENCODING,
244 HttpHeaders.CONNECTION,
245 HttpHeaders.CONTENT_LENGTH,
246 HttpHeaders.CONTENT_TYPE,
247 HttpHeaders.HOST
248 ].forEach(forEachFunc);
249 _headers.keys.forEach(forEachFunc);
250 }
251
252 @override
253 bool get chunkedTransferEncoding => null;
254
255 @override
256 void set chunkedTransferEncoding(bool _chunkedTransferEncoding) {
257 throw new UnsupportedError('chunked transfer is unsupported');
258 }
259
260 @override
261 int get contentLength => _request.contentLength;
262
263 @override
264 void set contentLength(int _contentLength) {
265 throw new UnsupportedError('content length is automatically set');
266 }
267
268 @override
269 void set date(DateTime _date) {
270 throw new UnsupportedError('date is unsupported');
271 }
272
273 @override
274 DateTime get date => null;
275
276 @override
277 void set expires(DateTime _expires) {
278 throw new UnsupportedError('expires is unsupported');
279 }
280
281 @override
282 DateTime get expires => null;
283
284 @override
285 void set host(String _host) {
286 throw new UnsupportedError('host is automatically set');
287 }
288
289 @override
290 String get host => _request.uri.host;
291
292 @override
293 DateTime get ifModifiedSince => null;
294
295 @override
296 void set ifModifiedSince(DateTime _ifModifiedSince) {
297 throw new UnsupportedError('if modified since is unsupported');
298 }
299
300 @override
301 void noFolding(String name) {
302 throw new UnsupportedError('no folding is unsupported');
303 }
304
305 @override
306 bool get persistentConnection => false;
307
308 @override
309 void set persistentConnection(bool _persistentConnection) {
310 throw new UnsupportedError('persistence connections are unsupported');
311 }
312
313 @override
314 void set port(int _port) {
315 throw new UnsupportedError('port is automatically set');
316 }
317
318 @override
319 int get port => _request.uri.port;
320
321 /// Clear all header key-value pairs.
322 @override
323 void clear() {
324 contentType = null;
325 _headers.clear();
326 }
327 }
328
329 /// HTTP response for a client connection.
330 class SyncHttpClientResponse {
331 /// The length of the body associated with the HTTP response.
332 int get contentLength => headers.contentLength;
333
334 /// The headers associated with the HTTP response.
335 final HttpHeaders headers;
336
337 /// A short textual description of the status code associated with the HTTP
338 /// response.
339 final String reasonPhrase;
340
341 /// The resulting HTTP status code associated with the HTTP response.
342 final int statusCode;
343
344 /// The body of the HTTP response.
345 final String body;
346
347 /// Creates an instance of [SyncHttpClientResponse] that contains the response
348 /// sent by the HTTP server over [socket].
349 factory SyncHttpClientResponse(RawSynchronousSocket socket) {
350 int statusCode;
351 String reasonPhrase;
352 StringBuffer body = new StringBuffer();
353 Map<String, List<String>> headers = {};
354
355 bool inHeader = false;
356 bool inBody = false;
357 int contentLength = 0;
358 int contentRead = 0;
359
360 void processLine(String line, int bytesRead, _LineDecoder decoder) {
361 if (inBody) {
362 body.write(line);
363 contentRead += bytesRead;
364 } else if (inHeader) {
365 if (line.trim().isEmpty) {
366 inBody = true;
367 if (contentLength > 0) {
368 decoder.expectedByteCount = contentLength;
369 }
370 return;
371 }
372 int separator = line.indexOf(':');
373 String name = line.substring(0, separator).toLowerCase().trim();
374 String value = line.substring(separator + 1).trim();
375 if (name == HttpHeaders.TRANSFER_ENCODING &&
376 value.toLowerCase() != 'identity') {
377 throw new UnsupportedError(
378 'only identity transfer encoding is accepted');
379 }
380 if (name == HttpHeaders.CONTENT_LENGTH) {
381 contentLength = int.parse(value);
382 }
383 if (!headers.containsKey(name)) {
384 headers[name] = [];
385 }
386 headers[name].add(value);
387 } else if (line.startsWith('HTTP/1.1') || line.startsWith('HTTP/1.0')) {
388 statusCode = int
389 .parse(line.substring('HTTP/1.x '.length, 'HTTP/1.x xxx'.length));
390 reasonPhrase = line.substring('HTTP/1.x xxx '.length);
391 inHeader = true;
392 } else {
393 throw new UnsupportedError('unsupported http response format');
394 }
395 }
396
397 var lineDecoder = new _LineDecoder.withCallback(processLine);
398
399 try {
400 while (!inHeader ||
401 !inBody ||
402 ((contentRead + lineDecoder.bufferedBytes) < contentLength)) {
403 var bytes = socket.readSync(1024);
404
405 if (bytes == null || bytes.length == 0) {
406 break;
407 }
408 lineDecoder.add(bytes);
409 }
410 } finally {
411 try {
412 lineDecoder.close();
413 } finally {
414 socket.closeSync();
415 }
416 }
417
418 return new SyncHttpClientResponse._(
419 reasonPhrase: reasonPhrase,
420 statusCode: statusCode,
421 body: body.toString(),
422 headers: headers);
423 }
424
425 SyncHttpClientResponse._(
426 {this.reasonPhrase, this.statusCode, this.body, headers})
427 : this.headers = new _SyncHttpClientResponseHeaders(headers);
428 }
429
430 class _SyncHttpClientResponseHeaders implements HttpHeaders {
431 final Map<String, List<String>> _headers;
432
433 _SyncHttpClientResponseHeaders(this._headers);
434
435 @override
436 List<String> operator [](String name) => _headers[name];
437
438 @override
439 void add(String name, Object value) {
440 throw new UnsupportedError('Response headers are immutable');
441 }
442
443 @override
444 bool get chunkedTransferEncoding => null;
445
446 @override
447 void set chunkedTransferEncoding(bool _chunkedTransferEncoding) {
448 throw new UnsupportedError('Response headers are immutable');
449 }
450
451 @override
452 int get contentLength {
453 String val = value(HttpHeaders.CONTENT_LENGTH);
454 if (val != null) {
455 return int.parse(val, onError: (_) => null);
456 }
457 return null;
458 }
459
460 @override
461 void set contentLength(int _contentLength) {
462 throw new UnsupportedError('Response headers are immutable');
463 }
464
465 @override
466 ContentType get contentType {
467 var val = value(HttpHeaders.CONTENT_TYPE);
468 if (val != null) {
469 return ContentType.parse(val);
470 }
471 return null;
472 }
473
474 @override
475 void set contentType(ContentType _contentType) {
476 throw new UnsupportedError('Response headers are immutable');
477 }
478
479 @override
480 void set date(DateTime _date) {
481 throw new UnsupportedError('Response headers are immutable');
482 }
483
484 @override
485 DateTime get date {
486 var val = value(HttpHeaders.DATE);
487 if (val != null) {
488 return DateTime.parse(val);
489 }
490 return null;
491 }
492
493 @override
494 void set expires(DateTime _expires) {
495 throw new UnsupportedError('Response headers are immutable');
496 }
497
498 @override
499 DateTime get expires {
500 var val = value(HttpHeaders.EXPIRES);
501 if (val != null) {
502 return DateTime.parse(val);
503 }
504 return null;
505 }
506
507 @override
508 void forEach(void f(String name, List<String> values)) => _headers.forEach(f);
509
510 @override
511 void set host(String _host) {
512 throw new UnsupportedError('Response headers are immutable');
513 }
514
515 @override
516 String get host {
517 var val = value(HttpHeaders.HOST);
518 if (val != null) {
519 return Uri.parse(val).host;
520 }
521 return null;
522 }
523
524 @override
525 DateTime get ifModifiedSince {
526 var val = value(HttpHeaders.IF_MODIFIED_SINCE);
527 if (val != null) {
528 return DateTime.parse(val);
529 }
530 return null;
531 }
532
533 @override
534 void set ifModifiedSince(DateTime _ifModifiedSince) {
535 throw new UnsupportedError('Response headers are immutable');
536 }
537
538 @override
539 void noFolding(String name) {
540 throw new UnsupportedError('Response headers are immutable');
541 }
542
543 @override
544 bool get persistentConnection => false;
545
546 @override
547 void set persistentConnection(bool _persistentConnection) {
548 throw new UnsupportedError('Response headers are immutable');
549 }
550
551 @override
552 void set port(int _port) {
553 throw new UnsupportedError('Response headers are immutable');
554 }
555
556 @override
557 int get port {
558 var val = value(HttpHeaders.HOST);
559 if (val != null) {
560 return Uri.parse(val).port;
561 }
562 return null;
563 }
564
565 @override
566 void remove(String name, Object value) {
567 throw new UnsupportedError('Response headers are immutable');
568 }
569
570 @override
571 void removeAll(String name) {
572 throw new UnsupportedError('Response headers are immutable');
573 }
574
575 @override
576 void set(String name, Object value) {
577 throw new UnsupportedError('Response headers are immutable');
578 }
579
580 @override
581 String value(String name) {
582 var val = this[name];
583 if (val == null || val.isEmpty) {
584 return null;
585 } else if (val.length == 1) {
586 return val[0];
587 } else {
588 throw new HttpException('header $name has more than one value');
589 }
590 }
591
592 @override
593 void clear() {
594 throw new UnsupportedError('Response headers are immutable');
595 }
596 }
OLDNEW
« no previous file with comments | « lib/src/line_decoder.dart ('k') | lib/sync_http.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698