UrlConnectionTransport.java
// SPDX-License-Identifier: Apache-2.0
package com.shengzhiai.yugu.stcompat.internal;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PushbackInputStream;
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.zip.GZIPInputStream;
/**
* {@link HttpTransport} on {@link HttpURLConnection}; no third-party HTTP client.
*
* <p>Requests carry {@code Accept-Encoding: gzip} and gzip answers are decompressed here instead of
* by the platform's HttpURLConnection, so the timing log knows the bytes on the wire as well as
* the bytes after decompression (SDK 2.1.1). Every attempt returns {@link HttpTransport.Metrics}.
*/
public final class UrlConnectionTransport implements HttpTransport {
/** Responses larger than this are truncated (results are a few dozen KB), after decompression too. */
static final int MAX_BODY = 8 * 1024 * 1024;
/**
* Stream the request body (fixed length) on Android. The desktop JDK implementation turns a
* 401 answer to a streamed body into an HttpRetryException and drops the error body, which
* would hide platform codes such as 2003; there the body is buffered instead (tests, JVM use).
*/
static final boolean STREAM_BODY = isAndroidRuntime();
/** java.vm.name is "Dalvik" on Dalvik and ART, java.vendor is "The Android Project". */
static boolean isAndroidRuntime() {
String vm = System.getProperty("java.vm.name", "").toLowerCase(java.util.Locale.ROOT);
String vendor = System.getProperty("java.vendor", "").toLowerCase(java.util.Locale.ROOT);
return vm.contains("dalvik") || vendor.contains("android");
}
@Override
public Response execute(Request request, CancelToken token) throws IOException {
Metrics m = new Metrics();
m.startNanos = System.nanoTime();
final HttpURLConnection c = (HttpURLConnection) new URL(request.url).openConnection();
Closeable abort = new Closeable() {
@Override
public void close() {
c.disconnect();
}
};
if (token != null && !token.attach(abort)) {
throw new IOException("cancelled");
}
try {
c.setConnectTimeout(request.connectTimeoutMs);
c.setReadTimeout(request.readTimeoutMs);
c.setRequestMethod(request.method);
c.setUseCaches(false);
c.setInstanceFollowRedirects(false);
boolean acceptEncoding = false;
for (Map.Entry<String, String> h : request.headers.entrySet()) {
c.setRequestProperty(h.getKey(), h.getValue());
acceptEncoding |= "accept-encoding".equalsIgnoreCase(h.getKey());
}
if (!acceptEncoding) {
// decompressed below: an explicit header turns off the transparent gzip of Android
c.setRequestProperty("Accept-Encoding", "gzip");
}
if (request.body != null) {
c.setDoOutput(true);
c.setRequestProperty("Content-Type", request.body.contentType());
long length = request.body.contentLength();
if (STREAM_BODY) {
c.setFixedLengthStreamingMode(length);
}
OutputStream out = c.getOutputStream();
try {
request.body.writeTo(out);
} finally {
out.close();
}
m.uploadBytes = length;
m.uploadDoneNanos = System.nanoTime();
}
int status;
try {
status = c.getResponseCode();
} catch (HttpRetryException e) {
// authentication challenge on a streamed body: the status is all that is left
return new Response(e.responseCode(), new LinkedHashMap<String, String>(), "");
}
m.headersNanos = System.nanoTime();
InputStream in = status >= 400 ? c.getErrorStream() : c.getInputStream();
String encoding = c.getContentEncoding();
m.contentEncoding = encoding == null || encoding.trim().isEmpty() ? null
: encoding.trim().toLowerCase(Locale.ROOT);
String body;
if (in == null) {
body = "";
m.wireBytes = 0;
m.bodyBytes = 0;
} else {
byte[] b = readBody(in, m);
body = new String(b, Codec.UTF_8);
}
m.bodyDoneNanos = System.nanoTime();
Map<String, String> headers = new LinkedHashMap<String, String>();
for (Map.Entry<String, List<String>> e : c.getHeaderFields().entrySet()) {
if (e.getKey() != null && e.getValue() != null && !e.getValue().isEmpty()) {
headers.put(e.getKey(), "server-timing".equalsIgnoreCase(e.getKey()) ? join(e.getValue())
: e.getValue().get(0));
}
}
return new Response(status, headers, body, m);
} finally {
if (token != null) {
token.detach(abort);
}
c.disconnect();
}
}
/** Server-Timing may come in several header lines; they form one comma separated list. */
private static String join(List<String> values) {
StringBuilder sb = new StringBuilder();
for (String v : values) {
if (v == null || v.trim().isEmpty()) {
continue;
}
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(v.trim());
}
return sb.toString();
}
/**
* Reads the body, gunzips it when the answer is gzip encoded and records the bytes on the wire
* and after decompression in {@code m}. Both are capped at {@link #MAX_BODY}.
*/
static byte[] readBody(InputStream raw, Metrics m) throws IOException {
CountingInputStream counted = new CountingInputStream(raw);
InputStream in = counted;
try {
if ("gzip".equals(m.contentEncoding) || "x-gzip".equals(m.contentEncoding)) {
PushbackInputStream p = new PushbackInputStream(counted, 1);
int first = p.read();
if (first < 0) {
m.wireBytes = 0;
m.bodyBytes = 0;
return new byte[0];
}
p.unread(first);
in = new GZIPInputStream(p, 8192);
}
byte[] b = readCapped(in);
m.wireBytes = counted.count;
m.bodyBytes = b.length;
return b;
} finally {
in.close();
}
}
private static byte[] readCapped(InputStream in) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) > 0) {
if (bos.size() + n > MAX_BODY) {
bos.write(buf, 0, MAX_BODY - bos.size());
break;
}
bos.write(buf, 0, n);
}
return bos.toByteArray();
}
/** Counts the bytes read through it. */
static final class CountingInputStream extends FilterInputStream {
long count;
CountingInputStream(InputStream in) {
super(in);
}
@Override
public int read() throws IOException {
int b = super.read();
if (b >= 0) {
count++;
}
return b;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int n = super.read(b, off, len);
if (n > 0) {
count += n;
}
return n;
}
@Override
public long skip(long n) throws IOException {
long s = super.skip(n);
if (s > 0) {
count += s;
}
return s;
}
}
}