1 // Copyright 2007 Google Inc. All rights reserved.
3 package com
.google
.appengine
.api
.urlfetch
;
5 import com
.google
.appengine
.api
.urlfetch
.URLFetchServicePb
.URLFetchRequest
;
6 import com
.google
.appengine
.api
.urlfetch
.URLFetchServicePb
.URLFetchRequest
.RequestMethod
;
7 import com
.google
.appengine
.api
.urlfetch
.URLFetchServicePb
.URLFetchResponse
;
8 import com
.google
.appengine
.api
.urlfetch
.URLFetchServicePb
.URLFetchResponse
.Header
;
9 import com
.google
.appengine
.api
.urlfetch
.URLFetchServicePb
.URLFetchServiceError
.ErrorCode
;
10 import com
.google
.appengine
.api
.utils
.FutureWrapper
;
11 import com
.google
.apphosting
.api
.ApiProxy
;
12 import com
.google
.protobuf
.ByteString
;
14 import java
.io
.IOException
;
15 import java
.net
.MalformedURLException
;
16 import java
.net
.SocketTimeoutException
;
18 import java
.net
.UnknownHostException
;
19 import java
.util
.ArrayList
;
20 import java
.util
.List
;
21 import java
.util
.concurrent
.Future
;
22 import java
.util
.logging
.Logger
;
24 import javax
.net
.ssl
.SSLHandshakeException
;
26 class URLFetchServiceImpl
implements URLFetchService
{
27 static final String PACKAGE
= "urlfetch";
29 private static final Logger logger
= Logger
.getLogger(URLFetchServiceImpl
.class.getName());
32 public HTTPResponse
fetch(URL url
) throws IOException
{
33 return fetch(new HTTPRequest(url
));
37 public HTTPResponse
fetch(HTTPRequest request
) throws IOException
{
40 responseBytes
= ApiProxy
.makeSyncCall(PACKAGE
, "Fetch", convertToPb(request
).toByteArray(),
41 createApiConfig(request
.getFetchOptions()));
42 } catch (ApiProxy
.RequestTooLargeException ex
) {
43 throw new IOException("The request exceeded the maximum permissible size");
44 } catch (ApiProxy
.ApplicationException ex
) {
45 Throwable cause
= convertApplicationException(request
.getURL(), ex
);
46 if (cause
instanceof RuntimeException
) {
47 throw (RuntimeException
) cause
;
48 } else if (cause
instanceof IOException
) {
49 throw (IOException
) cause
;
51 throw new RuntimeException(cause
);
53 } catch (ApiProxy
.ApiDeadlineExceededException ex
) {
54 throw new SocketTimeoutException("Timeout while fetching: " + request
.getURL());
57 URLFetchResponse responseProto
= URLFetchResponse
.newBuilder().mergeFrom(responseBytes
).build();
58 if (!request
.getFetchOptions().getAllowTruncate() && responseProto
.getContentWasTruncated()) {
59 throw new ResponseTooLargeException(request
.getURL().toString());
61 return convertFromPb(responseProto
);
65 public Future
<HTTPResponse
> fetchAsync(URL url
) {
66 return fetchAsync(new HTTPRequest(url
));
70 public Future
<HTTPResponse
> fetchAsync(HTTPRequest request
) {
71 final FetchOptions fetchOptions
= request
.getFetchOptions();
72 final URL url
= request
.getURL();
74 Future
<byte[]> response
= ApiProxy
.makeAsyncCall(
75 PACKAGE
, "Fetch", convertToPb(request
).toByteArray(), createApiConfig(fetchOptions
));
77 return new FutureWrapper
<byte[], HTTPResponse
>(response
) {
79 protected HTTPResponse
wrap(byte[] responseBytes
) throws IOException
{
80 URLFetchResponse responseProto
=
81 URLFetchResponse
.newBuilder().mergeFrom(responseBytes
).build();
82 if (!fetchOptions
.getAllowTruncate() && responseProto
.getContentWasTruncated()) {
83 throw new ResponseTooLargeException(url
.toString());
85 return convertFromPb(responseProto
);
89 protected Throwable
convertException(Throwable cause
) {
90 if (cause
instanceof ApiProxy
.ApplicationException
) {
91 return convertApplicationException(url
, (ApiProxy
.ApplicationException
) cause
);
92 } else if (cause
instanceof ApiProxy
.ApiDeadlineExceededException
) {
93 return new SocketTimeoutException("Timeout while fetching: " + url
);
100 static class DeadlineParser
{
101 static final DeadlineParser INSTANCE
= new DeadlineParser();
102 volatile int deadlineMs
= -1;
104 private DeadlineParser() {
109 String globalDefault
= System
.getProperty(URLFetchService
.DEFAULT_DEADLINE_PROPERTY
);
110 if (globalDefault
!= null) {
112 deadlineMs
= (int) (Double
.parseDouble(globalDefault
) * 1000);
113 } catch (NumberFormatException e
) {
115 logger
.warning("Cannot parse deadline: " + globalDefault
);
123 private ApiProxy
.ApiConfig
createApiConfig(FetchOptions options
) {
124 ApiProxy
.ApiConfig apiConfig
= new ApiProxy
.ApiConfig();
126 if (options
.getDeadline() != null) {
127 apiConfig
.setDeadlineInSeconds(options
.getDeadline());
128 } else if (DeadlineParser
.INSTANCE
.deadlineMs
>= 0) {
129 apiConfig
.setDeadlineInSeconds(DeadlineParser
.INSTANCE
.deadlineMs
/ 1000.0);
135 private String
getURLExceptionMessage(String formatString
, String url
, String errorDetail
) {
136 if (errorDetail
== null || errorDetail
.trim().isEmpty()) {
137 return String
.format(formatString
, url
);
139 return String
.format(formatString
+ ", error: %s", url
, errorDetail
);
142 private Throwable
convertApplicationException(URL requestUrl
, ApiProxy
.ApplicationException ex
) {
143 String urlString
= requestUrl
.toString();
144 ErrorCode errorCode
= ErrorCode
.valueOf(ex
.getApplicationError());
145 String errorDetail
= ex
.getErrorDetail();
148 return new MalformedURLException(
149 getURLExceptionMessage("Invalid URL specified: %s", urlString
, errorDetail
));
150 case PAYLOAD_TOO_LARGE
:
151 return new RequestPayloadTooLargeException(urlString
);
153 return new IOException(getURLExceptionMessage(
154 "Connection closed unexpectedly by server at URL: %s", urlString
, null));
155 case TOO_MANY_REDIRECTS
:
156 return new IOException(getURLExceptionMessage(
157 "Too many redirects at URL: %s with redirect=true", urlString
, null));
158 case MALFORMED_REPLY
:
159 return new IOException(getURLExceptionMessage(
160 "Malformed HTTP reply received from server at URL: %s", urlString
, errorDetail
));
161 case RESPONSE_TOO_LARGE
:
162 return new ResponseTooLargeException(urlString
);
164 return new UnknownHostException(
165 getURLExceptionMessage("DNS host lookup failed for URL: %s", urlString
, null));
167 return new IOException(
168 getURLExceptionMessage("Could not fetch URL: %s", urlString
, errorDetail
));
169 case INTERNAL_TRANSIENT_ERROR
:
170 return new InternalTransientException(urlString
);
171 case DEADLINE_EXCEEDED
:
172 return new SocketTimeoutException(
173 getURLExceptionMessage("Timeout while fetching URL: %s", urlString
, null));
174 case SSL_CERTIFICATE_ERROR
:
175 return new SSLHandshakeException(getURLExceptionMessage(
176 "Could not verify SSL certificate for URL: %s", urlString
, null));
177 case UNSPECIFIED_ERROR
:
179 return new IOException(ex
.getErrorDetail());
183 private URLFetchRequest
convertToPb(HTTPRequest request
) {
184 URLFetchRequest
.Builder requestProto
= URLFetchRequest
.newBuilder();
185 requestProto
.setUrl(request
.getURL().toExternalForm());
187 byte[] payload
= request
.getPayload();
188 if (payload
!= null) {
189 requestProto
.setPayload(ByteString
.copyFrom(payload
));
192 switch (request
.getMethod()) {
194 requestProto
.setMethod(RequestMethod
.GET
);
197 requestProto
.setMethod(RequestMethod
.POST
);
200 requestProto
.setMethod(RequestMethod
.HEAD
);
203 requestProto
.setMethod(RequestMethod
.PUT
);
206 requestProto
.setMethod(RequestMethod
.DELETE
);
209 requestProto
.setMethod(RequestMethod
.PATCH
);
212 throw new IllegalArgumentException("unknown method: " + request
.getMethod());
215 for (HTTPHeader header
: request
.getHeaders()) {
216 URLFetchRequest
.Header
.Builder headerProto
= URLFetchRequest
.Header
.newBuilder();
217 headerProto
.setKey(header
.getName());
218 headerProto
.setValue(header
.getValue());
219 requestProto
.addHeader(headerProto
);
222 requestProto
.setFollowRedirects(request
.getFetchOptions().getFollowRedirects());
224 switch (request
.getFetchOptions().getCertificateValidationBehavior()) {
226 requestProto
.setMustValidateServerCertificate(true);
228 case DO_NOT_VALIDATE
:
229 requestProto
.setMustValidateServerCertificate(false);
234 return requestProto
.build();
237 private HTTPResponse
convertFromPb(URLFetchResponse responseProto
) {
239 byte[] content
= responseProto
.hasContent() ? responseProto
.getContent().toByteArray() : null;
241 List
<HTTPHeader
> headers
= new ArrayList
<>(responseProto
.getHeaderCount());
242 for (Header header
: responseProto
.getHeaderList()) {
243 headers
.add(new HTTPHeader(header
.getKey(), header
.getValue()));
247 if (responseProto
.hasFinalUrl() && responseProto
.getFinalUrl().length() > 0) {
249 finalURL
= new URL(responseProto
.getFinalUrl());
250 } catch (MalformedURLException e
) {
251 logger
.severe("malformed final URL: " + e
);
255 return new HTTPResponse(responseProto
.getStatusCode(), content
, finalURL
, headers
);