14
14
import java .io .IOException ;
15
15
import java .io .InputStream ;
16
16
import java .net .HttpURLConnection ;
17
+ import java .net .MalformedURLException ;
17
18
import java .net .URISyntaxException ;
18
19
import java .net .URL ;
19
20
import java .util .Map ;
22
23
public class HttpUrlFetcher implements DataFetcher <InputStream > {
23
24
private static final String TAG = "HttpUrlFetcher" ;
24
25
private static final int MAXIMUM_REDIRECTS = 5 ;
26
+ @ VisibleForTesting static final String REDIRECT_HEADER_FIELD = "Location" ;
25
27
26
28
@ VisibleForTesting
27
29
static final HttpUrlConnectionFactory DEFAULT_CONNECTION_FACTORY =
28
30
new DefaultHttpUrlConnectionFactory ();
29
31
/** Returned when a connection error prevented us from receiving an http error. */
30
- private static final int INVALID_STATUS_CODE = -1 ;
32
+ @ VisibleForTesting static final int INVALID_STATUS_CODE = -1 ;
31
33
32
34
private final GlideUrl glideUrl ;
33
35
private final int timeout ;
@@ -68,59 +70,97 @@ public void loadData(
68
70
}
69
71
70
72
private InputStream loadDataWithRedirects (
71
- URL url , int redirects , URL lastUrl , Map <String , String > headers ) throws IOException {
73
+ URL url , int redirects , URL lastUrl , Map <String , String > headers ) throws HttpException {
72
74
if (redirects >= MAXIMUM_REDIRECTS ) {
73
- throw new HttpException ("Too many (> " + MAXIMUM_REDIRECTS + ") redirects!" );
75
+ throw new HttpException (
76
+ "Too many (> " + MAXIMUM_REDIRECTS + ") redirects!" , INVALID_STATUS_CODE );
74
77
} else {
75
78
// Comparing the URLs using .equals performs additional network I/O and is generally broken.
76
79
// See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html.
77
80
try {
78
81
if (lastUrl != null && url .toURI ().equals (lastUrl .toURI ())) {
79
- throw new HttpException ("In re-direct loop" );
82
+ throw new HttpException ("In re-direct loop" , INVALID_STATUS_CODE );
80
83
}
81
84
} catch (URISyntaxException e ) {
82
85
// Do nothing, this is best effort.
83
86
}
84
87
}
85
88
86
- urlConnection = connectionFactory .build (url );
87
- for (Map .Entry <String , String > headerEntry : headers .entrySet ()) {
88
- urlConnection .addRequestProperty (headerEntry .getKey (), headerEntry .getValue ());
89
- }
90
- urlConnection .setConnectTimeout (timeout );
91
- urlConnection .setReadTimeout (timeout );
92
- urlConnection .setUseCaches (false );
93
- urlConnection .setDoInput (true );
89
+ urlConnection = buildAndConfigureConnection (url , headers );
94
90
95
- // Stop the urlConnection instance of HttpUrlConnection from following redirects so that
96
- // redirects will be handled by recursive calls to this method, loadDataWithRedirects.
97
- urlConnection .setInstanceFollowRedirects (false );
91
+ try {
92
+ // Connect explicitly to avoid errors in decoders if connection fails.
93
+ urlConnection .connect ();
94
+ // Set the stream so that it's closed in cleanup to avoid resource leaks. See #2352.
95
+ stream = urlConnection .getInputStream ();
96
+ } catch (IOException e ) {
97
+ throw new HttpException (
98
+ "Failed to connect or obtain data" , getHttpStatusCodeOrInvalid (urlConnection ), e );
99
+ }
98
100
99
- // Connect explicitly to avoid errors in decoders if connection fails.
100
- urlConnection .connect ();
101
- // Set the stream so that it's closed in cleanup to avoid resource leaks. See #2352.
102
- stream = urlConnection .getInputStream ();
103
101
if (isCancelled ) {
104
102
return null ;
105
103
}
106
- final int statusCode = urlConnection .getResponseCode ();
104
+
105
+ final int statusCode = getHttpStatusCodeOrInvalid (urlConnection );
107
106
if (isHttpOk (statusCode )) {
108
107
return getStreamForSuccessfulRequest (urlConnection );
109
108
} else if (isHttpRedirect (statusCode )) {
110
- String redirectUrlString = urlConnection .getHeaderField ("Location" );
109
+ String redirectUrlString = urlConnection .getHeaderField (REDIRECT_HEADER_FIELD );
111
110
if (TextUtils .isEmpty (redirectUrlString )) {
112
- throw new HttpException ("Received empty or null redirect url" );
111
+ throw new HttpException ("Received empty or null redirect url" , statusCode );
112
+ }
113
+ URL redirectUrl ;
114
+ try {
115
+ redirectUrl = new URL (url , redirectUrlString );
116
+ } catch (MalformedURLException e ) {
117
+ throw new HttpException ("Bad redirect url: " + redirectUrlString , statusCode , e );
113
118
}
114
- URL redirectUrl = new URL (url , redirectUrlString );
115
119
// Closing the stream specifically is required to avoid leaking ResponseBodys in addition
116
120
// to disconnecting the url connection below. See #2352.
117
121
cleanup ();
118
122
return loadDataWithRedirects (redirectUrl , redirects + 1 , url , headers );
119
123
} else if (statusCode == INVALID_STATUS_CODE ) {
120
124
throw new HttpException (statusCode );
121
125
} else {
122
- throw new HttpException (urlConnection .getResponseMessage (), statusCode );
126
+ try {
127
+ throw new HttpException (urlConnection .getResponseMessage (), statusCode );
128
+ } catch (IOException e ) {
129
+ throw new HttpException ("Failed to get a response message" , statusCode , e );
130
+ }
131
+ }
132
+ }
133
+
134
+ private static int getHttpStatusCodeOrInvalid (HttpURLConnection urlConnection ) {
135
+ try {
136
+ return urlConnection .getResponseCode ();
137
+ } catch (IOException e ) {
138
+ if (Log .isLoggable (TAG , Log .DEBUG )) {
139
+ Log .d (TAG , "Failed to get a response code" , e );
140
+ }
141
+ }
142
+ return INVALID_STATUS_CODE ;
143
+ }
144
+
145
+ private HttpURLConnection buildAndConfigureConnection (URL url , Map <String , String > headers )
146
+ throws HttpException {
147
+ HttpURLConnection urlConnection ;
148
+ try {
149
+ urlConnection = connectionFactory .build (url );
150
+ } catch (IOException e ) {
151
+ throw new HttpException ("URL.openConnection threw" , /*statusCode=*/ 0 , e );
152
+ }
153
+ for (Map .Entry <String , String > headerEntry : headers .entrySet ()) {
154
+ urlConnection .addRequestProperty (headerEntry .getKey (), headerEntry .getValue ());
123
155
}
156
+ urlConnection .setConnectTimeout (timeout );
157
+ urlConnection .setReadTimeout (timeout );
158
+ urlConnection .setUseCaches (false );
159
+ urlConnection .setDoInput (true );
160
+ // Stop the urlConnection instance of HttpUrlConnection from following redirects so that
161
+ // redirects will be handled by recursive calls to this method, loadDataWithRedirects.
162
+ urlConnection .setInstanceFollowRedirects (false );
163
+ return urlConnection ;
124
164
}
125
165
126
166
// Referencing constants is less clear than a simple static method.
@@ -134,15 +174,20 @@ private static boolean isHttpRedirect(int statusCode) {
134
174
}
135
175
136
176
private InputStream getStreamForSuccessfulRequest (HttpURLConnection urlConnection )
137
- throws IOException {
138
- if (TextUtils .isEmpty (urlConnection .getContentEncoding ())) {
139
- int contentLength = urlConnection .getContentLength ();
140
- stream = ContentLengthInputStream .obtain (urlConnection .getInputStream (), contentLength );
141
- } else {
142
- if (Log .isLoggable (TAG , Log .DEBUG )) {
143
- Log .d (TAG , "Got non empty content encoding: " + urlConnection .getContentEncoding ());
177
+ throws HttpException {
178
+ try {
179
+ if (TextUtils .isEmpty (urlConnection .getContentEncoding ())) {
180
+ int contentLength = urlConnection .getContentLength ();
181
+ stream = ContentLengthInputStream .obtain (urlConnection .getInputStream (), contentLength );
182
+ } else {
183
+ if (Log .isLoggable (TAG , Log .DEBUG )) {
184
+ Log .d (TAG , "Got non empty content encoding: " + urlConnection .getContentEncoding ());
185
+ }
186
+ stream = urlConnection .getInputStream ();
144
187
}
145
- stream = urlConnection .getInputStream ();
188
+ } catch (IOException e ) {
189
+ throw new HttpException (
190
+ "Failed to obtain InputStream" , getHttpStatusCodeOrInvalid (urlConnection ), e );
146
191
}
147
192
return stream ;
148
193
}
0 commit comments