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 |