| OLD | NEW |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /// Helpers for dealing with HTTP. | 5 /// Helpers for dealing with HTTP. |
| 6 library pub.http; | 6 library pub.http; |
| 7 | 7 |
| 8 import 'dart:async'; | 8 import 'dart:async'; |
| 9 import 'dart:convert'; | 9 import 'dart:convert'; |
| 10 import 'dart:io'; | 10 import 'dart:io'; |
| (...skipping 13 matching lines...) Expand all Loading... |
| 24 final HTTP_TIMEOUT = 30 * 1000; | 24 final HTTP_TIMEOUT = 30 * 1000; |
| 25 | 25 |
| 26 /// Headers and field names that should be censored in the log output. | 26 /// Headers and field names that should be censored in the log output. |
| 27 final _CENSORED_FIELDS = const ['refresh_token', 'authorization']; | 27 final _CENSORED_FIELDS = const ['refresh_token', 'authorization']; |
| 28 | 28 |
| 29 /// Headers required for pub.dartlang.org API requests. | 29 /// Headers required for pub.dartlang.org API requests. |
| 30 /// | 30 /// |
| 31 /// The Accept header tells pub.dartlang.org which version of the API we're | 31 /// The Accept header tells pub.dartlang.org which version of the API we're |
| 32 /// expecting, so it can either serve that version or give us a 406 error if | 32 /// expecting, so it can either serve that version or give us a 406 error if |
| 33 /// it's not supported. | 33 /// it's not supported. |
| 34 final PUB_API_HEADERS = const {'Accept': 'application/vnd.pub.v2+json'}; | 34 final PUB_API_HEADERS = const { |
| 35 'Accept': 'application/vnd.pub.v2+json' |
| 36 }; |
| 35 | 37 |
| 36 /// An HTTP client that transforms 40* errors and socket exceptions into more | 38 /// An HTTP client that transforms 40* errors and socket exceptions into more |
| 37 /// user-friendly error messages. | 39 /// user-friendly error messages. |
| 38 /// | 40 /// |
| 39 /// This also adds a 30-second timeout to every request. This can be configured | 41 /// This also adds a 30-second timeout to every request. This can be configured |
| 40 /// on a per-request basis by setting the 'Pub-Request-Timeout' header to the | 42 /// on a per-request basis by setting the 'Pub-Request-Timeout' header to the |
| 41 /// desired number of milliseconds, or to "None" to disable the timeout. | 43 /// desired number of milliseconds, or to "None" to disable the timeout. |
| 42 class _PubHttpClient extends http.BaseClient { | 44 class _PubHttpClient extends http.BaseClient { |
| 43 final _requestStopwatches = new Map<http.BaseRequest, Stopwatch>(); | 45 final _requestStopwatches = new Map<http.BaseRequest, Stopwatch>(); |
| 44 | 46 |
| (...skipping 23 matching lines...) Expand all Loading... |
| 68 timeoutLength = int.parse(timeoutString); | 70 timeoutLength = int.parse(timeoutString); |
| 69 } | 71 } |
| 70 | 72 |
| 71 var future = _inner.send(request).then((streamedResponse) { | 73 var future = _inner.send(request).then((streamedResponse) { |
| 72 _logResponse(streamedResponse); | 74 _logResponse(streamedResponse); |
| 73 | 75 |
| 74 var status = streamedResponse.statusCode; | 76 var status = streamedResponse.statusCode; |
| 75 // 401 responses should be handled by the OAuth2 client. It's very | 77 // 401 responses should be handled by the OAuth2 client. It's very |
| 76 // unlikely that they'll be returned by non-OAuth2 requests. We also want | 78 // unlikely that they'll be returned by non-OAuth2 requests. We also want |
| 77 // to pass along 400 responses from the token endpoint. | 79 // to pass along 400 responses from the token endpoint. |
| 78 var tokenRequest = urisEqual( | 80 var tokenRequest = |
| 79 streamedResponse.request.url, oauth2.tokenEndpoint); | 81 urisEqual(streamedResponse.request.url, oauth2.tokenEndpoint); |
| 80 if (status < 400 || status == 401 || (status == 400 && tokenRequest)) { | 82 if (status < 400 || status == 401 || (status == 400 && tokenRequest)) { |
| 81 return streamedResponse; | 83 return streamedResponse; |
| 82 } | 84 } |
| 83 | 85 |
| 84 if (status == 406 && | 86 if (status == 406 && |
| 85 request.headers['Accept'] == PUB_API_HEADERS['Accept']) { | 87 request.headers['Accept'] == PUB_API_HEADERS['Accept']) { |
| 86 fail("Pub ${sdk.version} is incompatible with the current version of " | 88 fail( |
| 87 "${request.url.host}.\n" | 89 "Pub ${sdk.version} is incompatible with the current version of " |
| 88 "Upgrade pub to the latest version and try again."); | 90 "${request.url.host}.\n" "Upgrade pub to the latest version and
try again."); |
| 89 } | 91 } |
| 90 | 92 |
| 91 if (status == 500 && | 93 if (status == 500 && |
| 92 (request.url.host == "pub.dartlang.org" || | 94 (request.url.host == "pub.dartlang.org" || |
| 93 request.url.host == "storage.googleapis.com")) { | 95 request.url.host == "storage.googleapis.com")) { |
| 94 var message = "HTTP error 500: Internal Server Error at " | 96 var message = |
| 95 "${request.url}."; | 97 "HTTP error 500: Internal Server Error at " "${request.url}."; |
| 96 | 98 |
| 97 if (request.url.host == "pub.dartlang.org" || | 99 if (request.url.host == "pub.dartlang.org" || |
| 98 request.url.host == "storage.googleapis.com") { | 100 request.url.host == "storage.googleapis.com") { |
| 99 message += "\nThis is likely a transient error. Please try again " | 101 message += |
| 100 "later."; | 102 "\nThis is likely a transient error. Please try again " "later."; |
| 101 } | 103 } |
| 102 | 104 |
| 103 fail(message); | 105 fail(message); |
| 104 } | 106 } |
| 105 | 107 |
| 106 return http.Response.fromStream(streamedResponse).then((response) { | 108 return http.Response.fromStream(streamedResponse).then((response) { |
| 107 throw new PubHttpException(response); | 109 throw new PubHttpException(response); |
| 108 }); | 110 }); |
| 109 }).catchError((error, stackTrace) { | 111 }).catchError((error, stackTrace) { |
| 110 if (error is SocketException && | 112 if (error is SocketException && error.osError != null) { |
| 111 error.osError != null) { | |
| 112 if (error.osError.errorCode == 8 || | 113 if (error.osError.errorCode == 8 || |
| 113 error.osError.errorCode == -2 || | 114 error.osError.errorCode == -2 || |
| 114 error.osError.errorCode == -5 || | 115 error.osError.errorCode == -5 || |
| 115 error.osError.errorCode == 11001 || | 116 error.osError.errorCode == 11001 || |
| 116 error.osError.errorCode == 11004) { | 117 error.osError.errorCode == 11004) { |
| 117 fail('Could not resolve URL "${request.url.origin}".', | 118 fail( |
| 118 error, stackTrace); | 119 'Could not resolve URL "${request.url.origin}".', |
| 120 error, |
| 121 stackTrace); |
| 119 } else if (error.osError.errorCode == -12276) { | 122 } else if (error.osError.errorCode == -12276) { |
| 120 fail('Unable to validate SSL certificate for ' | 123 fail( |
| 121 '"${request.url.origin}".', | 124 'Unable to validate SSL certificate for ' '"${request.url.origin}"
.', |
| 122 error, stackTrace); | 125 error, |
| 126 stackTrace); |
| 123 } | 127 } |
| 124 } | 128 } |
| 125 throw error; | 129 throw error; |
| 126 }); | 130 }); |
| 127 | 131 |
| 128 if (timeoutLength == null) return future; | 132 if (timeoutLength == null) return future; |
| 129 return timeout(future, timeoutLength, request.url, | 133 return timeout( |
| 134 future, |
| 135 timeoutLength, |
| 136 request.url, |
| 130 'fetching URL "${request.url}"'); | 137 'fetching URL "${request.url}"'); |
| 131 } | 138 } |
| 132 | 139 |
| 133 /// Logs the fact that [request] was sent, and information about it. | 140 /// Logs the fact that [request] was sent, and information about it. |
| 134 void _logRequest(http.BaseRequest request) { | 141 void _logRequest(http.BaseRequest request) { |
| 135 var requestLog = new StringBuffer(); | 142 var requestLog = new StringBuffer(); |
| 136 requestLog.writeln("HTTP ${request.method} ${request.url}"); | 143 requestLog.writeln("HTTP ${request.method} ${request.url}"); |
| 137 request.headers.forEach((name, value) => | 144 request.headers.forEach( |
| 138 requestLog.writeln(_logField(name, value))); | 145 (name, value) => requestLog.writeln(_logField(name, value))); |
| 139 | 146 |
| 140 if (request.method == 'POST') { | 147 if (request.method == 'POST') { |
| 141 var contentTypeString = request.headers[HttpHeaders.CONTENT_TYPE]; | 148 var contentTypeString = request.headers[HttpHeaders.CONTENT_TYPE]; |
| 142 if (contentTypeString == null) contentTypeString = ''; | 149 if (contentTypeString == null) contentTypeString = ''; |
| 143 var contentType = ContentType.parse(contentTypeString); | 150 var contentType = ContentType.parse(contentTypeString); |
| 144 if (request is http.MultipartRequest) { | 151 if (request is http.MultipartRequest) { |
| 145 requestLog.writeln(); | 152 requestLog.writeln(); |
| 146 requestLog.writeln("Body fields:"); | 153 requestLog.writeln("Body fields:"); |
| 147 request.fields.forEach((name, value) => | 154 request.fields.forEach( |
| 148 requestLog.writeln(_logField(name, value))); | 155 (name, value) => requestLog.writeln(_logField(name, value))); |
| 149 | 156 |
| 150 // TODO(nweiz): make MultipartRequest.files readable, and log them? | 157 // TODO(nweiz): make MultipartRequest.files readable, and log them? |
| 151 } else if (request is http.Request) { | 158 } else if (request is http.Request) { |
| 152 if (contentType.value == 'application/x-www-form-urlencoded') { | 159 if (contentType.value == 'application/x-www-form-urlencoded') { |
| 153 requestLog.writeln(); | 160 requestLog.writeln(); |
| 154 requestLog.writeln("Body fields:"); | 161 requestLog.writeln("Body fields:"); |
| 155 request.bodyFields.forEach((name, value) => | 162 request.bodyFields.forEach( |
| 156 requestLog.writeln(_logField(name, value))); | 163 (name, value) => requestLog.writeln(_logField(name, value))); |
| 157 } else if (contentType.value == 'text/plain' || | 164 } else if (contentType.value == 'text/plain' || |
| 158 contentType.value == 'application/json') { | 165 contentType.value == 'application/json') { |
| 159 requestLog.write(request.body); | 166 requestLog.write(request.body); |
| 160 } | 167 } |
| 161 } | 168 } |
| 162 } | 169 } |
| 163 | 170 |
| 164 log.fine(requestLog.toString().trim()); | 171 log.fine(requestLog.toString().trim()); |
| 165 } | 172 } |
| 166 | 173 |
| 167 /// Logs the fact that [response] was received, and information about it. | 174 /// Logs the fact that [response] was received, and information about it. |
| 168 void _logResponse(http.StreamedResponse response) { | 175 void _logResponse(http.StreamedResponse response) { |
| 169 // TODO(nweiz): Fork the response stream and log the response body. Be | 176 // TODO(nweiz): Fork the response stream and log the response body. Be |
| 170 // careful not to log OAuth2 private data, though. | 177 // careful not to log OAuth2 private data, though. |
| 171 | 178 |
| 172 var responseLog = new StringBuffer(); | 179 var responseLog = new StringBuffer(); |
| 173 var request = response.request; | 180 var request = response.request; |
| 174 var stopwatch = _requestStopwatches.remove(request)..stop(); | 181 var stopwatch = _requestStopwatches.remove(request)..stop(); |
| 175 responseLog.writeln("HTTP response ${response.statusCode} " | 182 responseLog.writeln( |
| 176 "${response.reasonPhrase} for ${request.method} ${request.url}"); | 183 "HTTP response ${response.statusCode} " |
| 184 "${response.reasonPhrase} for ${request.method} ${request.url}"); |
| 177 responseLog.writeln("took ${stopwatch.elapsed}"); | 185 responseLog.writeln("took ${stopwatch.elapsed}"); |
| 178 response.headers.forEach((name, value) => | 186 response.headers.forEach( |
| 179 responseLog.writeln(_logField(name, value))); | 187 (name, value) => responseLog.writeln(_logField(name, value))); |
| 180 | 188 |
| 181 log.fine(responseLog.toString().trim()); | 189 log.fine(responseLog.toString().trim()); |
| 182 } | 190 } |
| 183 | 191 |
| 184 /// Returns a log-formatted string for the HTTP field or header with the given | 192 /// Returns a log-formatted string for the HTTP field or header with the given |
| 185 /// [name] and [value]. | 193 /// [name] and [value]. |
| 186 String _logField(String name, String value) { | 194 String _logField(String name, String value) { |
| 187 if (_CENSORED_FIELDS.contains(name.toLowerCase())) { | 195 if (_CENSORED_FIELDS.contains(name.toLowerCase())) { |
| 188 return "$name: <censored>"; | 196 return "$name: <censored>"; |
| 189 } else { | 197 } else { |
| (...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 242 value = JSON.decode(response.body); | 250 value = JSON.decode(response.body); |
| 243 } on FormatException catch (e) { | 251 } on FormatException catch (e) { |
| 244 invalidServerResponse(response); | 252 invalidServerResponse(response); |
| 245 } | 253 } |
| 246 if (value is! Map) invalidServerResponse(response); | 254 if (value is! Map) invalidServerResponse(response); |
| 247 return value; | 255 return value; |
| 248 } | 256 } |
| 249 | 257 |
| 250 /// Throws an error describing an invalid response from the server. | 258 /// Throws an error describing an invalid response from the server. |
| 251 void invalidServerResponse(http.Response response) => | 259 void invalidServerResponse(http.Response response) => |
| 252 fail('Invalid server response:\n${response.body}'); | 260 fail('Invalid server response:\n${response.body}'); |
| 253 | 261 |
| 254 /// Exception thrown when an HTTP operation fails. | 262 /// Exception thrown when an HTTP operation fails. |
| 255 class PubHttpException implements Exception { | 263 class PubHttpException implements Exception { |
| 256 final http.Response response; | 264 final http.Response response; |
| 257 | 265 |
| 258 const PubHttpException(this.response); | 266 const PubHttpException(this.response); |
| 259 | 267 |
| 260 String toString() => 'HTTP error ${response.statusCode}: ' | 268 String toString() => |
| 261 '${response.reasonPhrase}'; | 269 'HTTP error ${response.statusCode}: ' '${response.reasonPhrase}'; |
| 262 } | 270 } |
| OLD | NEW |