UrlConnectionExecutor.java

package org.jsoup.helper;

import org.jsoup.Connection;
import org.jsoup.internal.Functions;
import org.jspecify.annotations.Nullable;

import javax.net.ssl.HttpsURLConnection;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static org.jsoup.helper.HttpConnection.Response;

/**
 Execute HTTP requests using the HttpURLConnection implementation. The HttpClient is used by default if available; set system property
 {@code jsoup.useHttpClient} to {@code false} to explicitly prefer the HttpUrlConnection.
 */
class UrlConnectionExecutor extends RequestExecutor {
    @Nullable
    HttpURLConnection conn;

    UrlConnectionExecutor(HttpConnection.Request req, HttpConnection.@Nullable Response prevRes) {
        super(req, prevRes);
    }

    @Override
    HttpConnection.Response execute() throws IOException {
        try {
            conn = createConnection(req);
            conn.connect();
            if (conn.getDoOutput()) {
                try (OutputStream out = conn.getOutputStream()) {
                    Response.writePost(req, out);
                } catch (IOException e) {
                    conn.disconnect();
                    throw e;
                }
            }

            // set up url, method, header, cookies
            Response res = new Response(req);
            res.executor = this;
            res.method = Connection.Method.valueOf(conn.getRequestMethod());
            res.url = conn.getURL();
            res.statusCode = conn.getResponseCode();
            res.statusMessage = conn.getResponseMessage();
            if (res.statusMessage == null) res.statusMessage = ""; // getResponseMessage may be null but statusMessage() is not null
            res.contentType = conn.getContentType();
            res.contentLength = conn.getContentLength();
            Map<String, List<String>> resHeaders = createHeaderMap(conn);
            res.prepareResponse(resHeaders, prevRes);

            return res;
        } catch (IOException e) {
            safeClose();
            throw e;
        }
    }

    @Override
    InputStream responseBody() throws IOException {
        if (conn == null) throw new IllegalStateException("Not yet executed");
        return conn.getErrorStream() != null ? conn.getErrorStream() : conn.getInputStream();
    }

    @Override
    void safeClose() {
        if (conn != null) {
            conn.disconnect();
            conn = null;
        }
    }

    // set up connection defaults, and details from request
    private static HttpURLConnection createConnection(HttpConnection.Request req) throws IOException {
        Proxy proxy = req.proxy();
        final HttpURLConnection conn = (HttpURLConnection) (
            proxy == null ?
                req.url().openConnection() :
                req.url().openConnection(proxy)
        );

        conn.setRequestMethod(req.method().name());
        conn.setInstanceFollowRedirects(false); // don't rely on native redirection support
        conn.setConnectTimeout(req.timeout());
        conn.setReadTimeout(req.timeout() / 2); // gets reduced after connection is made and status is read

        if (conn instanceof HttpsURLConnection) {
            HttpsURLConnection scon = (HttpsURLConnection) conn;
            if (req.sslContext != null)
                scon.setSSLSocketFactory(req.sslContext.getSocketFactory());
            else if (req.sslSocketFactory() != null)
                scon.setSSLSocketFactory(req.sslSocketFactory());
        }
        if (req.authenticator != null)
            AuthenticationHandler.handler.enable(req.authenticator, conn); // removed in finally
        if (req.method().hasBody())
            conn.setDoOutput(true);
        CookieUtil.applyCookiesToRequest(req, conn::addRequestProperty); // from the Request key/val cookies and the Cookie Store
        for (Map.Entry<String, List<String>> header : req.multiHeaders().entrySet()) {
            for (String value : header.getValue()) {
                conn.addRequestProperty(header.getKey(), value);
            }
        }
        return conn;
    }

    private static LinkedHashMap<String, List<String>> createHeaderMap(HttpURLConnection conn) {
        // the default sun impl of conn.getHeaderFields() returns header values out of order
        final LinkedHashMap<String, List<String>> headers = new LinkedHashMap<>();
        int i = 0;
        while (true) {
            final String key = conn.getHeaderFieldKey(i);
            final String val = conn.getHeaderField(i);
            if (key == null && val == null)
                break;
            i++;
            if (key == null || val == null)
                continue; // skip http1.1 line

            final List<String> vals = headers.computeIfAbsent(key, Functions.listFunction());
            vals.add(val);
        }
        return headers;
    }
}