All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.squareup.okhttp.internal.http.HttpEngine Maven / Gradle / Ivy

There is a newer version: 2.7.5
Show newest version
/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.squareup.okhttp.internal.http;

import com.squareup.okhttp.Connection;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.Route;
import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.InternalCache;
import com.squareup.okhttp.internal.Version;
import java.io.IOException;
import java.io.InputStream;
import java.net.CookieHandler;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URL;
import java.security.cert.CertificateException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.GzipSource;
import okio.Okio;
import okio.Sink;
import okio.Source;

import static com.squareup.okhttp.internal.Util.closeQuietly;
import static com.squareup.okhttp.internal.Util.getDefaultPort;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
import static com.squareup.okhttp.internal.http.StatusLine.HTTP_CONTINUE;
import static com.squareup.okhttp.internal.http.StatusLine.HTTP_PERM_REDIRECT;
import static com.squareup.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
import static java.net.HttpURLConnection.HTTP_MULT_CHOICE;
import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;

/**
 * Handles a single HTTP request/response pair. Each HTTP engine follows this
 * lifecycle:
 * 
    *
  1. It is created. *
  2. The HTTP request message is sent with sendRequest(). Once the request * is sent it is an error to modify the request headers. After * sendRequest() has been called the request body can be written to if * it exists. *
  3. The HTTP response message is read with readResponse(). After the * response has been read the response headers and body can be read. * All responses have a response body input stream, though in some * instances this stream is empty. *
* *

The request and response may be served by the HTTP response cache, by the * network, or by both in the event of a conditional GET. */ public final class HttpEngine { /** * How many redirects should we follow? Chrome follows 21; Firefox, curl, * and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5. */ public static final int MAX_REDIRECTS = 20; private static final ResponseBody EMPTY_BODY = new ResponseBody() { @Override public MediaType contentType() { return null; } @Override public long contentLength() { return 0; } @Override public BufferedSource source() { return new Buffer(); } }; final OkHttpClient client; private Connection connection; private RouteSelector routeSelector; private Route route; private final Response priorResponse; private Transport transport; /** The time when the request headers were written, or -1 if they haven't been written yet. */ long sentRequestMillis = -1; /** * True if this client added an "Accept-Encoding: gzip" header field and is * therefore responsible for also decompressing the transfer stream. */ private boolean transparentGzip; /** * True if the request body must be completely buffered before transmission; * false if it can be streamed. Buffering has two advantages: we don't need * the content-length in advance and we can retransmit if necessary. The * upside of streaming is that we can save memory. */ public final boolean bufferRequestBody; /** * The original application-provided request. Never modified by OkHttp. When * follow-up requests are necessary, they are derived from this request. */ private final Request userRequest; /** * The request to send on the network, or null for no network request. This is * derived from the user request, and customized to support OkHttp features * like compression and caching. */ private Request networkRequest; /** * The cached response, or null if the cache doesn't exist or cannot be used * for this request. Conditional caching means this may be non-null even when * the network request is non-null. Never modified by OkHttp. */ private Response cacheResponse; /** * The response read from the network. Null if the network response hasn't * been read yet, or if the network is not used. Never modified by OkHttp. */ private Response networkResponse; /** * The user-visible response. This is derived from either the network * response, cache response, or both. It is customized to support OkHttp * features like compression and caching. */ private Response userResponse; private Sink requestBodyOut; private BufferedSink bufferedRequestBody; /** Null until a response is received from the network or the cache. */ private Source responseTransferSource; private BufferedSource responseBody; private InputStream responseBodyBytes; /** The cache request currently being populated from a network response. */ private CacheRequest storeRequest; private CacheStrategy cacheStrategy; /** * @param request the HTTP request without a body. The body must be * written via the engine's request body stream. * @param connection the connection used for an intermediate response * immediately prior to this request/response pair, such as a same-host * redirect. This engine assumes ownership of the connection and must * release it when it is unneeded. * @param routeSelector the route selector used for a failed attempt * immediately preceding this attempt, or null if this request doesn't * recover from a failure. */ public HttpEngine(OkHttpClient client, Request request, boolean bufferRequestBody, Connection connection, RouteSelector routeSelector, RetryableSink requestBodyOut, Response priorResponse) { this.client = client; this.userRequest = request; this.bufferRequestBody = bufferRequestBody; this.connection = connection; this.routeSelector = routeSelector; this.requestBodyOut = requestBodyOut; this.priorResponse = priorResponse; if (connection != null) { Internal.instance.setOwner(connection, this); this.route = connection.getRoute(); } else { this.route = null; } } /** * Figures out what the response source will be, and opens a socket to that * source if necessary. Prepares the request headers and gets ready to start * writing the request body if it exists. */ public void sendRequest() throws IOException { if (cacheStrategy != null) return; // Already sent. if (transport != null) throw new IllegalStateException(); Request request = networkRequest(userRequest); InternalCache responseCache = Internal.instance.internalCache(client); Response cacheCandidate = responseCache != null ? responseCache.get(request) : null; long now = System.currentTimeMillis(); cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get(); networkRequest = cacheStrategy.networkRequest; cacheResponse = cacheStrategy.cacheResponse; if (responseCache != null) { responseCache.trackResponse(cacheStrategy); } if (cacheCandidate != null && cacheResponse == null) { closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it. } if (networkRequest != null) { // Open a connection unless we inherited one from a redirect. if (connection == null) { connect(networkRequest); } transport = Internal.instance.newTransport(connection, this); // Create a request body if we don't have one already. We'll already have // one if we're retrying a failed POST. if (permitsRequestBody() && requestBodyOut == null) { long contentLength = OkHeaders.contentLength(request); if (bufferRequestBody) { if (contentLength > Integer.MAX_VALUE) { throw new IllegalStateException("Use setFixedLengthStreamingMode() or " + "setChunkedStreamingMode() for requests larger than 2 GiB."); } if (contentLength != -1) { // Buffer a request body of a known length. transport.writeRequestHeaders(request); requestBodyOut = new RetryableSink((int) contentLength); } else { // Buffer a request body of an unknown length. Don't write request // headers until the entire body is ready; otherwise we can't set the // Content-Length header correctly. requestBodyOut = new RetryableSink(); } } else { transport.writeRequestHeaders(request); requestBodyOut = transport.createRequestBody(request, contentLength); } } } else { // We aren't using the network. Recycle a connection we may have inherited from a redirect. if (connection != null) { Internal.instance.recycle(client.getConnectionPool(), connection); connection = null; } if (cacheResponse != null) { // We have a valid cached response. Promote it to the user response immediately. this.userResponse = cacheResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .cacheResponse(stripBody(cacheResponse)) .build(); } else { // We're forbidden from using the network, and the cache is insufficient. this.userResponse = new Response.Builder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .protocol(Protocol.HTTP_1_1) .code(504) .message("Unsatisfiable Request (only-if-cached)") .body(EMPTY_BODY) .build(); } if (userResponse.body() != null) { initContentStream(userResponse.body().source()); } } } private static Response stripBody(Response response) { return response != null && response.body() != null ? response.newBuilder().body(null).build() : response; } /** Connect to the origin server either directly or via a proxy. */ private void connect(Request request) throws IOException { if (connection != null) throw new IllegalStateException(); if (routeSelector == null) { routeSelector = RouteSelector.get(request, client); } connection = routeSelector.next(this); route = connection.getRoute(); } /** * Called immediately before the transport transmits HTTP request headers. * This is used to observe the sent time should the request be cached. */ public void writingRequestHeaders() { if (sentRequestMillis != -1) throw new IllegalStateException(); sentRequestMillis = System.currentTimeMillis(); } boolean permitsRequestBody() { return HttpMethod.permitsRequestBody(userRequest.method()); } /** Returns the request body or null if this request doesn't have a body. */ public Sink getRequestBody() { if (cacheStrategy == null) throw new IllegalStateException(); return requestBodyOut; } public BufferedSink getBufferedRequestBody() { BufferedSink result = bufferedRequestBody; if (result != null) return result; Sink requestBody = getRequestBody(); return requestBody != null ? (bufferedRequestBody = Okio.buffer(requestBody)) : null; } public boolean hasResponse() { return userResponse != null; } public Request getRequest() { return userRequest; } /** Returns the engine's response. */ // TODO: the returned body will always be null. public Response getResponse() { if (userResponse == null) throw new IllegalStateException(); return userResponse; } public BufferedSource getResponseBody() { if (userResponse == null) throw new IllegalStateException(); return responseBody; } public InputStream getResponseBodyBytes() { InputStream result = responseBodyBytes; return result != null ? result : (responseBodyBytes = Okio.buffer(getResponseBody()).inputStream()); } public Connection getConnection() { return connection; } /** * Report and attempt to recover from {@code e}. Returns a new HTTP engine * that should be used for the retry if {@code e} is recoverable, or null if * the failure is permanent. Requests with a body can only be recovered if the * body is buffered. */ public HttpEngine recover(IOException e, Sink requestBodyOut) { if (routeSelector != null && connection != null) { routeSelector.connectFailed(connection, e); } boolean canRetryRequestBody = requestBodyOut == null || requestBodyOut instanceof RetryableSink; if (routeSelector == null && connection == null // No connection. || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt. || !isRecoverable(e) || !canRetryRequestBody) { return null; } Connection connection = close(); // For failure recovery, use the same route selector with a new connection. return new HttpEngine(client, userRequest, bufferRequestBody, connection, routeSelector, (RetryableSink) requestBodyOut, priorResponse); } public HttpEngine recover(IOException e) { return recover(e, requestBodyOut); } private boolean isRecoverable(IOException e) { // If the problem was a CertificateException from the X509TrustManager, // do not retry, we didn't have an abrupt server-initiated exception. boolean sslFailure = e instanceof SSLPeerUnverifiedException || (e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException); boolean protocolFailure = e instanceof ProtocolException; return !sslFailure && !protocolFailure; } /** * Returns the route used to retrieve the response. Null if we haven't * connected yet, or if no connection was necessary. */ public Route getRoute() { return route; } private void maybeCache() throws IOException { InternalCache responseCache = Internal.instance.internalCache(client); if (responseCache == null) return; // Should we cache this response for this request? if (!CacheStrategy.isCacheable(userResponse, networkRequest)) { if (HttpMethod.invalidatesCache(networkRequest.method())) { try { responseCache.remove(networkRequest); } catch (IOException ignored) { // The cache cannot be written. } } return; } // Offer this request to the cache. storeRequest = responseCache.put(stripBody(userResponse)); } /** * Configure the socket connection to be either pooled or closed when it is * either exhausted or closed. If it is unneeded when this is called, it will * be released immediately. */ public void releaseConnection() throws IOException { if (transport != null && connection != null) { transport.releaseConnectionOnIdle(); } connection = null; } /** * Immediately closes the socket connection if it's currently held by this * engine. Use this to interrupt an in-flight request from any thread. It's * the caller's responsibility to close the request body and response body * streams; otherwise resources may be leaked. */ public void disconnect() { if (transport != null) { try { transport.disconnect(this); } catch (IOException ignored) { } } } /** * Release any resources held by this engine. If a connection is still held by * this engine, it is returned. */ public Connection close() { if (bufferedRequestBody != null) { // This also closes the wrapped requestBodyOut. closeQuietly(bufferedRequestBody); } else if (requestBodyOut != null) { closeQuietly(requestBodyOut); } // If this engine never achieved a response body, its connection cannot be reused. if (responseBody == null) { if (connection != null) closeQuietly(connection.getSocket()); // TODO: does this break SPDY? connection = null; return null; } // Close the response body. This will recycle the connection if it is eligible. closeQuietly(responseBody); // Clear the buffer held by the response body input stream adapter. closeQuietly(responseBodyBytes); // Close the connection if it cannot be reused. if (transport != null && connection != null && !transport.canReuseConnection()) { closeQuietly(connection.getSocket()); connection = null; return null; } // Prevent this engine from disconnecting a connection it no longer owns. if (connection != null && !Internal.instance.clearOwner(connection)) { connection = null; } Connection result = connection; connection = null; return result; } /** * Initialize the response content stream from the response transfer source. * These two sources are the same unless we're doing transparent gzip, in * which case the content source is decompressed. * *

Whenever we do transparent gzip we also strip the corresponding headers. * We strip the Content-Encoding header to prevent the application from * attempting to double decompress. We strip the Content-Length header because * it is the length of the compressed content, but the application is only * interested in the length of the uncompressed content. * *

This method should only be used for non-empty response bodies. Response * codes like "304 Not Modified" can include "Content-Encoding: gzip" without * a response body and we will crash if we attempt to decompress the zero-byte * source. */ private void initContentStream(Source transferSource) throws IOException { responseTransferSource = transferSource; if (transparentGzip && "gzip".equalsIgnoreCase(userResponse.header("Content-Encoding"))) { userResponse = userResponse.newBuilder() .removeHeader("Content-Encoding") .removeHeader("Content-Length") .build(); responseBody = Okio.buffer(new GzipSource(transferSource)); } else { responseBody = Okio.buffer(transferSource); } } /** * Returns true if the response must have a (possibly 0-length) body. * See RFC 2616 section 4.3. */ public boolean hasResponseBody() { // HEAD requests never yield a body regardless of the response headers. if (userRequest.method().equals("HEAD")) { return false; } int responseCode = userResponse.code(); if ((responseCode < HTTP_CONTINUE || responseCode >= 200) && responseCode != HTTP_NO_CONTENT && responseCode != HTTP_NOT_MODIFIED) { return true; } // If the Content-Length or Transfer-Encoding headers disagree with the // response code, the response is malformed. For best compatibility, we // honor the headers. if (OkHeaders.contentLength(networkResponse) != -1 || "chunked".equalsIgnoreCase(networkResponse.header("Transfer-Encoding"))) { return true; } return false; } /** * Populates request with defaults and cookies. * *

This client doesn't specify a default {@code Accept} header because it * doesn't know what content types the application is interested in. */ private Request networkRequest(Request request) throws IOException { Request.Builder result = request.newBuilder(); if (request.header("Host") == null) { result.header("Host", hostHeader(request.url())); } if ((connection == null || connection.getProtocol() != Protocol.HTTP_1_0) && request.header("Connection") == null) { result.header("Connection", "Keep-Alive"); } if (request.header("Accept-Encoding") == null) { transparentGzip = true; result.header("Accept-Encoding", "gzip"); } CookieHandler cookieHandler = client.getCookieHandler(); if (cookieHandler != null) { // Capture the request headers added so far so that they can be offered to the CookieHandler. // This is mostly to stay close to the RI; it is unlikely any of the headers above would // affect cookie choice besides "Host". Map> headers = OkHeaders.toMultimap(result.build().headers(), null); Map> cookies = cookieHandler.get(request.uri(), headers); // Add any new cookies to the request. OkHeaders.addCookies(result, cookies); } if (request.header("User-Agent") == null) { result.header("User-Agent", Version.userAgent()); } return result.build(); } public static String hostHeader(URL url) { return getEffectivePort(url) != getDefaultPort(url.getProtocol()) ? url.getHost() + ":" + url.getPort() : url.getHost(); } /** * Flushes the remaining request header and body, parses the HTTP response * headers and starts reading the HTTP response body if it exists. */ public void readResponse() throws IOException { if (userResponse != null) { return; // Already ready. } if (networkRequest == null && cacheResponse == null) { throw new IllegalStateException("call sendRequest() first!"); } if (networkRequest == null) { return; // No network response to read. } // Flush the request body if there's data outstanding. if (bufferedRequestBody != null && bufferedRequestBody.buffer().size() > 0) { bufferedRequestBody.flush(); } if (sentRequestMillis == -1) { if (OkHeaders.contentLength(networkRequest) == -1 && requestBodyOut instanceof RetryableSink) { // We might not learn the Content-Length until the request body has been buffered. long contentLength = ((RetryableSink) requestBodyOut).contentLength(); networkRequest = networkRequest.newBuilder() .header("Content-Length", Long.toString(contentLength)) .build(); } transport.writeRequestHeaders(networkRequest); } if (requestBodyOut != null) { if (bufferedRequestBody != null) { // This also closes the wrapped requestBodyOut. bufferedRequestBody.close(); } else { requestBodyOut.close(); } if (requestBodyOut instanceof RetryableSink) { transport.writeRequestBody((RetryableSink) requestBodyOut); } } transport.flushRequest(); networkResponse = transport.readResponseHeaders() .request(networkRequest) .handshake(connection.getHandshake()) .header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis)) .header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis())) .build(); Internal.instance.setProtocol(connection, networkResponse.protocol()); receiveHeaders(networkResponse.headers()); // If we have a cache response too, then we're doing a conditional get. if (cacheResponse != null) { if (validate(cacheResponse, networkResponse)) { userResponse = cacheResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .headers(combine(cacheResponse.headers(), networkResponse.headers())) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); transport.emptyTransferStream(); releaseConnection(); // Update the cache after combining headers but before stripping the // Content-Encoding header (as performed by initContentStream()). InternalCache responseCache = Internal.instance.internalCache(client); responseCache.trackConditionalCacheHit(); responseCache.update(cacheResponse, stripBody(userResponse)); if (cacheResponse.body() != null) { initContentStream(cacheResponse.body().source()); } return; } else { closeQuietly(cacheResponse.body()); } } userResponse = networkResponse.newBuilder() .request(userRequest) .priorResponse(stripBody(priorResponse)) .cacheResponse(stripBody(cacheResponse)) .networkResponse(stripBody(networkResponse)) .build(); if (!hasResponseBody()) { // Don't call initContentStream() when the response doesn't have any content. responseTransferSource = transport.getTransferStream(storeRequest); responseBody = Okio.buffer(responseTransferSource); return; } maybeCache(); initContentStream(transport.getTransferStream(storeRequest)); } /** * Returns true if {@code cached} should be used; false if {@code network} * response should be used. */ private static boolean validate(Response cached, Response network) { if (network.code() == HTTP_NOT_MODIFIED) { return true; } // The HTTP spec says that if the network's response is older than our // cached response, we may return the cache's response. Like Chrome (but // unlike Firefox), this client prefers to return the newer response. Date lastModified = cached.headers().getDate("Last-Modified"); if (lastModified != null) { Date networkLastModified = network.headers().getDate("Last-Modified"); if (networkLastModified != null && networkLastModified.getTime() < lastModified.getTime()) { return true; } } return false; } /** * Combines cached headers with a network headers as defined by RFC 2616, * 13.5.3. */ private static Headers combine(Headers cachedHeaders, Headers networkHeaders) throws IOException { Headers.Builder result = new Headers.Builder(); for (int i = 0; i < cachedHeaders.size(); i++) { String fieldName = cachedHeaders.name(i); String value = cachedHeaders.value(i); if ("Warning".equalsIgnoreCase(fieldName) && value.startsWith("1")) { continue; // Drop 100-level freshness warnings. } if (!OkHeaders.isEndToEnd(fieldName) || networkHeaders.get(fieldName) == null) { result.add(fieldName, value); } } for (int i = 0; i < networkHeaders.size(); i++) { String fieldName = networkHeaders.name(i); if ("Content-Length".equalsIgnoreCase(fieldName)) { continue; // Ignore content-length headers of validating responses. } if (OkHeaders.isEndToEnd(fieldName)) { result.add(fieldName, networkHeaders.value(i)); } } return result.build(); } public void receiveHeaders(Headers headers) throws IOException { CookieHandler cookieHandler = client.getCookieHandler(); if (cookieHandler != null) { cookieHandler.put(userRequest.uri(), OkHeaders.toMultimap(headers, null)); } } /** * Figures out the HTTP request to make in response to receiving this engine's * response. This will either add authentication headers or follow redirects. * If a follow-up is either unnecessary or not applicable, this returns null. */ public Request followUpRequest() throws IOException { if (userResponse == null) throw new IllegalStateException(); Proxy selectedProxy = getRoute() != null ? getRoute().getProxy() : client.getProxy(); int responseCode = userResponse.code(); switch (responseCode) { case HTTP_PROXY_AUTH: if (selectedProxy.type() != Proxy.Type.HTTP) { throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy"); } // fall-through case HTTP_UNAUTHORIZED: return OkHeaders.processAuthHeader(client.getAuthenticator(), userResponse, selectedProxy); case HTTP_PERM_REDIRECT: case HTTP_TEMP_REDIRECT: // "If the 307 or 308 status code is received in response to a request other than GET // or HEAD, the user agent MUST NOT automatically redirect the request" if (!userRequest.method().equals("GET") && !userRequest.method().equals("HEAD")) { return null; } // fall-through case HTTP_MULT_CHOICE: case HTTP_MOVED_PERM: case HTTP_MOVED_TEMP: case HTTP_SEE_OTHER: // Does the client allow redirects? if (!client.getFollowRedirects()) return null; String location = userResponse.header("Location"); if (location == null) return null; URL url = new URL(userRequest.url(), location); // Don't follow redirects to unsupported protocols. if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) return null; // If configured, don't follow redirects between SSL and non-SSL. boolean sameProtocol = url.getProtocol().equals(userRequest.url().getProtocol()); if (!sameProtocol && !client.getFollowSslRedirects()) return null; // Redirects don't include a request body. Request.Builder requestBuilder = userRequest.newBuilder(); if (HttpMethod.permitsRequestBody(userRequest.method())) { requestBuilder.method("GET", null); requestBuilder.removeHeader("Transfer-Encoding"); requestBuilder.removeHeader("Content-Length"); requestBuilder.removeHeader("Content-Type"); } // When redirecting across hosts, drop all authentication headers. This // is potentially annoying to the application layer since they have no // way to retain them. if (!sameConnection(url)) { requestBuilder.removeHeader("Authorization"); } return requestBuilder.url(url).build(); default: return null; } } /** * Returns true if an HTTP request for {@code followUp} can reuse the * connection used by this engine. */ public boolean sameConnection(URL followUp) { URL url = userRequest.url(); return url.getHost().equals(followUp.getHost()) && getEffectivePort(url) == getEffectivePort(followUp) && url.getProtocol().equals(followUp.getProtocol()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy