Improve HTTP redirect following (#1637)

This implements HTTP redirect following ourselves.

The goal is:

1. All I/O is checked against `--allowed-resources` and
`--allowed-modules`, including HTTP redirects
2. HTTP rewrite rules can affect redirect following
3. HTTP headers can affect redirect following

---------

Co-authored-by: Islon Scherer <islonscherer@gmail.com>
This commit is contained in:
Daniel Chao
2026-06-08 11:13:48 -07:00
committed by GitHub
parent b993cc3bb1
commit d012285f7d
36 changed files with 465 additions and 129 deletions
@@ -1295,7 +1295,7 @@ result = someLib.x
CliBaseOptions(
sourceModules = listOf(moduleUri),
workingDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
)
)
val buffer = ByteArrayOutputStream()
@@ -1337,7 +1337,7 @@ result = someLib.x
CliBaseOptions(
sourceModules = listOf(moduleUri),
workingDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
settings = settingsFile,
)
)
@@ -1367,7 +1367,7 @@ result = someLib.x
workingDir = tempDir,
moduleCacheDir = tempDir,
noCache = true,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = packageServer.port,
)
)
@@ -1473,7 +1473,7 @@ result = someLib.x
sourceModules = listOf(URI("package://localhost:1/birds@0.5.0#/catalog/Ostrich.pkl")),
noCache = true,
httpProxy = URI(wwRuntimeInfo.httpBaseUrl),
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
allowedModules = SecurityManagers.defaultAllowedModules + Pattern.compile("http:"),
)
)
@@ -44,7 +44,7 @@ class CliPackageDownloaderTest {
baseOptions =
CliBaseOptions(
moduleCacheDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = server.port,
),
packageUris =
@@ -83,7 +83,7 @@ class CliPackageDownloaderTest {
baseOptions =
CliBaseOptions(
workingDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = server.port,
),
packageUris = listOf(PackageUri("package://localhost:0/birds@0.5.0")),
@@ -103,7 +103,7 @@ class CliPackageDownloaderTest {
baseOptions =
CliBaseOptions(
moduleCacheDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = server.port,
),
packageUris =
@@ -124,7 +124,7 @@ class CliPackageDownloaderTest {
baseOptions =
CliBaseOptions(
moduleCacheDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = server.port,
),
packageUris =
@@ -165,7 +165,7 @@ class CliPackageDownloaderTest {
baseOptions =
CliBaseOptions(
moduleCacheDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = server.port,
),
packageUris = listOf(PackageUri("package://localhost:0/badChecksum@1.0.0")),
@@ -184,7 +184,7 @@ class CliPackageDownloaderTest {
baseOptions =
CliBaseOptions(
moduleCacheDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = server.port,
),
packageUris =
@@ -221,7 +221,7 @@ class CliPackageDownloaderTest {
baseOptions =
CliBaseOptions(
moduleCacheDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = server.port,
),
packageUris = listOf(PackageUri("package://localhost:0/birds@0.5.0")),
@@ -967,7 +967,7 @@ class CliProjectPackagerTest {
CliProjectPackager(
CliBaseOptions(
workingDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = packageServer.port,
),
listOf(tempDir.resolve("project")),
@@ -1011,7 +1011,7 @@ class CliProjectPackagerTest {
CliProjectPackager(
CliBaseOptions(
workingDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = packageServer.port,
),
listOf(tempDir.resolve("project")),
@@ -87,7 +87,7 @@ class CliProjectResolverTest {
CliProjectResolver(
CliBaseOptions(
workingDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = packageServer.port,
noCache = true,
),
@@ -142,7 +142,7 @@ class CliProjectResolverTest {
CliProjectResolver(
CliBaseOptions(
workingDir = tempDir,
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = packageServer.port,
noCache = true,
),
@@ -240,7 +240,7 @@ class CliProjectResolverTest {
)
CliProjectResolver(
CliBaseOptions(
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = packageServer.port,
noCache = true,
),
@@ -322,7 +322,7 @@ class CliProjectResolverTest {
val errOut = StringWriter()
CliProjectResolver(
CliBaseOptions(
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = packageServer.port,
noCache = true,
),
@@ -397,7 +397,7 @@ class CliProjectResolverTest {
val errOut = StringWriter()
CliProjectResolver(
CliBaseOptions(
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = packageServer.port,
noCache = true,
),
@@ -484,7 +484,7 @@ class CliProjectResolverTest {
assertThatCode {
CliProjectResolver(
CliBaseOptions(
caCertificates = listOf(FileTestUtils.selfSignedCertificate),
caCertificates = listOf(FileTestUtils.selfSignedCertificatePem),
testPort = packageServer.port,
noCache = true,
),
@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,12 +31,19 @@ object FileTestUtils {
?: workingDir.parent.parent.takeIf { it.resolve("settings.gradle.kts").exists() }
?: throw AssertionError("Failed to locate root project directory.")
}
val selfSignedCertificate: Path by lazy {
val selfSignedCertificateP12: Path by lazy {
rootProjectDir.resolve("pkl-commons-test/build/keystore/localhost.p12")
}
val selfSignedCertificatePem: Path by lazy {
rootProjectDir.resolve("pkl-commons-test/build/keystore/localhost.pem")
}
val selfSignedCertificatePassword = "password"
fun writeCertificateWithMissingLines(dir: Path): Path {
val lines = selfSignedCertificate.readLines()
val lines = selfSignedCertificatePem.readLines()
// drop some lines in the middle
return dir.resolve("invalidCerts.pem").writeLines(lines.take(5) + lines.takeLast(5))
}
+1
View File
@@ -67,6 +67,7 @@ dependencies {
implementation(libs.snakeYaml)
testImplementation(projects.pklCommonsTest)
testImplementation(libs.wiremock)
add("generatorImplementation", libs.javaPoet)
add("generatorImplementation", libs.truffleApi)
@@ -25,7 +25,7 @@ import org.graalvm.polyglot.Context;
import org.jspecify.annotations.Nullable;
import org.pkl.core.evaluatorSettings.TraceMode;
import org.pkl.core.http.HttpClient;
import org.pkl.core.http.HttpClientInitException;
import org.pkl.core.http.HttpClientException;
import org.pkl.core.module.ModuleKeyFactory;
import org.pkl.core.module.ProjectDependenciesManager;
import org.pkl.core.packages.PackageLoadError;
@@ -79,10 +79,7 @@ public class Analyzer {
context.enter();
var vmContext = VmContext.get(null);
return VmImportAnalyzer.analyze(sources, vmContext);
} catch (SecurityManagerException
| IOException
| PackageLoadError
| HttpClientInitException e) {
} catch (SecurityManagerException | IOException | PackageLoadError | HttpClientException e) {
throw new PklException(e.getMessage(), e);
} catch (PklException err) {
throw err;
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,7 @@ import com.oracle.truffle.api.source.SourceSection;
import java.util.Map;
import org.pkl.core.SecurityManagerException;
import org.pkl.core.ast.ExpressionNode;
import org.pkl.core.http.HttpClientInitException;
import org.pkl.core.http.HttpClientException;
import org.pkl.core.module.ResolvedModuleKey;
import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.runtime.VmContext;
@@ -60,7 +60,7 @@ public final class ImportGlobMemberBodyNode extends ExpressionNode {
context.getSecurityManager().checkImportModule(currentModule.getUri(), importUri);
var moduleToImport = context.getModuleResolver().resolve(importUri, this);
return language.loadModule(moduleToImport, this);
} catch (SecurityManagerException | PackageLoadError | HttpClientInitException e) {
} catch (SecurityManagerException | PackageLoadError | HttpClientException e) {
throw exceptionBuilder().withCause(e).build();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,7 +26,7 @@ import java.net.URI;
import org.pkl.core.SecurityManagerException;
import org.pkl.core.ast.member.SharedMemberNode;
import org.pkl.core.externalreader.ExternalReaderProcessException;
import org.pkl.core.http.HttpClientInitException;
import org.pkl.core.http.HttpClientException;
import org.pkl.core.module.ResolvedModuleKey;
import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.runtime.VmContext;
@@ -96,7 +96,7 @@ public class ImportGlobNode extends AbstractImportNode {
return cachedResult;
} catch (IOException e) {
throw exceptionBuilder().evalError("ioErrorResolvingGlob", importUri).withCause(e).build();
} catch (SecurityManagerException | HttpClientInitException e) {
} catch (SecurityManagerException | HttpClientException e) {
throw exceptionBuilder().withCause(e).build();
} catch (PackageLoadError e) {
throw exceptionBuilder().adhocEvalError(e.getMessage()).build();
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,7 +22,7 @@ import com.oracle.truffle.api.nodes.NodeInfo;
import com.oracle.truffle.api.source.SourceSection;
import java.net.URI;
import org.pkl.core.SecurityManagerException;
import org.pkl.core.http.HttpClientInitException;
import org.pkl.core.http.HttpClientException;
import org.pkl.core.module.ResolvedModuleKey;
import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.runtime.VmContext;
@@ -55,7 +55,7 @@ public final class ImportNode extends AbstractImportNode {
context.getSecurityManager().checkImportModule(currentModule.getUri(), importUri);
var moduleToImport = context.getModuleResolver().resolve(importUri, this);
importedModule = language.loadModule(moduleToImport, this);
} catch (SecurityManagerException | PackageLoadError | HttpClientInitException e) {
} catch (SecurityManagerException | PackageLoadError | HttpClientException e) {
throw exceptionBuilder().withCause(e).build();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,7 +27,7 @@ import org.graalvm.collections.EconomicMap;
import org.pkl.core.SecurityManagerException;
import org.pkl.core.ast.member.SharedMemberNode;
import org.pkl.core.externalreader.ExternalReaderProcessException;
import org.pkl.core.http.HttpClientInitException;
import org.pkl.core.http.HttpClientException;
import org.pkl.core.module.ModuleKey;
import org.pkl.core.runtime.VmContext;
import org.pkl.core.runtime.VmLanguage;
@@ -97,7 +97,7 @@ public abstract class ReadGlobNode extends AbstractReadNode {
return cachedResult;
} catch (IOException e) {
throw exceptionBuilder().evalError("ioErrorResolvingGlob", globPattern).withCause(e).build();
} catch (SecurityManagerException | HttpClientInitException | URISyntaxException e) {
} catch (SecurityManagerException | HttpClientException | URISyntaxException e) {
throw exceptionBuilder().withCause(e).build();
} catch (InvalidGlobPatternException e) {
throw exceptionBuilder()
@@ -24,7 +24,10 @@ import java.net.http.HttpResponse.BodyHandler;
@ThreadSafe
final class DummyHttpClient implements HttpClient {
@Override
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler) {
public <T> HttpResponse<T> send(
HttpRequest request,
BodyHandler<T> responseBodyHandler,
HttpRequestChecker httpRequestChecker) {
throw new AssertionError("Dummy HTTP client cannot send request: " + request);
}
@@ -19,12 +19,14 @@ import java.io.IOException;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpTimeoutException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLContext;
import org.jspecify.annotations.Nullable;
import org.pkl.core.SecurityManagerException;
/**
* An HTTP client.
@@ -190,7 +192,7 @@ public interface HttpClient extends AutoCloseable {
/**
* Creates a new {@code HttpClient} from the current state of this builder.
*
* @throws HttpClientInitException if an error occurs while initializing the client
* @throws HttpClientException if an error occurs while initializing the client
*/
HttpClient build();
@@ -242,11 +244,14 @@ public interface HttpClient extends AutoCloseable {
* java.net.http.HttpClient#send}.
*
* @throws IOException if an I/O error occurs when sending or receiving
* @throws HttpClientInitException if an error occurs while initializing a {@linkplain
* Builder#buildLazily lazy} client
* @throws HttpClientException if a known (user-presentable) error occurs.
* @throws SecurityManagerException based on {@code httpRequestChecker}
*/
<T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
throws IOException;
<T> HttpResponse<T> send(
HttpRequest request,
BodyHandler<T> responseBodyHandler,
HttpRequestChecker httpRequestChecker)
throws IOException, SecurityManagerException;
/**
* Closes this client.
@@ -258,4 +263,9 @@ public interface HttpClient extends AutoCloseable {
* {@link IllegalStateException}.
*/
void close();
@FunctionalInterface
interface HttpRequestChecker {
void check(URI uri) throws SecurityManagerException;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,19 +16,19 @@
package org.pkl.core.http;
/**
* Indicates that an error occurred while initializing an HTTP client. A common example is an error
* reading or parsing a certificate.
* Indicates that an error occurred while initializing an HTTP client, or when making an HTTP call.
* The error messages are user-presentable.
*/
public final class HttpClientInitException extends RuntimeException {
public HttpClientInitException(String message) {
public final class HttpClientException extends RuntimeException {
public HttpClientException(String message) {
super(message);
}
public HttpClientInitException(Throwable cause) {
public HttpClientException(Throwable cause) {
super(cause);
}
public HttpClientInitException(String message, Throwable cause) {
public HttpClientException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -46,6 +46,7 @@ import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManagerFactory;
import org.pkl.core.SecurityManagerException;
import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.Exceptions;
@@ -83,13 +84,16 @@ final class JdkHttpClient implements HttpClient {
.sslContext(createSslContext(certificateFiles, certificateBytes))
.connectTimeout(connectTimeout)
.proxy(proxySelector)
.followRedirects(Redirect.NORMAL)
.followRedirects(Redirect.NEVER)
.build();
}
@Override
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler)
throws IOException {
public <T> HttpResponse<T> send(
HttpRequest request,
BodyHandler<T> responseBodyHandler,
HttpRequestChecker httpRequestChecker)
throws IOException, SecurityManagerException {
try {
return underlying.send(request, responseBodyHandler);
} catch (ConnectException e) {
@@ -144,7 +148,7 @@ final class JdkHttpClient implements HttpClient {
return sslContext;
} catch (GeneralSecurityException | IOException e) {
throw new HttpClientInitException(
throw new HttpClientException(
ErrorMessages.create("cannotInitHttpClient", Exceptions.getRootReason(e)), e);
}
}
@@ -156,9 +160,9 @@ final class JdkHttpClient implements HttpClient {
try (var stream = Files.newInputStream(file)) {
collectCertificates(certificates, factory, stream, file);
} catch (NoSuchFileException e) {
throw new HttpClientInitException(ErrorMessages.create("cannotFindCertFile", file));
throw new HttpClientException(ErrorMessages.create("cannotFindCertFile", file));
} catch (IOException e) {
throw new HttpClientInitException(
throw new HttpClientException(
ErrorMessages.create("cannotReadCertFile", Exceptions.getRootReason(e)));
}
}
@@ -179,11 +183,11 @@ final class JdkHttpClient implements HttpClient {
//noinspection unchecked
certificates = (Collection<X509Certificate>) factory.generateCertificates(stream);
} catch (CertificateException e) {
throw new HttpClientInitException(
throw new HttpClientException(
ErrorMessages.create("cannotParseCertFile", source, Exceptions.getRootReason(e)));
}
if (certificates.isEmpty()) {
throw new HttpClientInitException(ErrorMessages.create("emptyCertFile", source));
throw new HttpClientException(ErrorMessages.create("emptyCertFile", source));
}
anchors.addAll(certificates);
}
@@ -24,6 +24,7 @@ import java.net.http.HttpResponse.BodyHandler;
import java.util.Optional;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.pkl.core.SecurityManagerException;
/**
* An {@code HttpClient} decorator that defers creating the underlying HTTP client until the first
@@ -45,9 +46,12 @@ final class LazyHttpClient implements HttpClient {
}
@Override
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler)
throws IOException {
return getOrCreateClient().send(request, responseBodyHandler);
public <T> HttpResponse<T> send(
HttpRequest request,
BodyHandler<T> responseBodyHandler,
HttpRequestChecker httpRequestChecker)
throws IOException, SecurityManagerException {
return getOrCreateClient().send(request, responseBodyHandler, httpRequestChecker);
}
@Override
@@ -34,6 +34,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
import org.pkl.core.PklBugException;
import org.pkl.core.SecurityManagerException;
import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.HttpUtils;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Pair;
@@ -42,13 +44,15 @@ import org.pkl.core.util.Pair;
* An {@code HttpClient} decorator that
*
* <ul>
* <li>overrides the {@code User-Agent} header of {@code HttpRequest}s
* <li>overrides the headers of {@code HttpRequest}s
* <li>sets a request timeout if none is present
* <li>ensures that {@link #close()} is idempotent.
* <li>rewrites outbound URI prefixes with another prefix.
* <li>handles redirects, rewriting URLs after each redirect and checking against {@link
* org.pkl.core.http.HttpClient.HttpRequestChecker}.
* </ul>
*
* <p>Both {@code User-Agent} header and default request timeout are configurable through {@link
* <p>The headers, default request timeout, and URI rewrites are configurable through {@link
* HttpClient.Builder}.
*/
@ThreadSafe
@@ -64,6 +68,22 @@ final class RequestRewritingClient implements HttpClient {
private final AtomicBoolean closed = new AtomicBoolean();
private static final int MAX_HTTP_REDIRECTS;
static {
// allow Java users to configure max redirects the same way they would Java's default HTTP
// client.
var maxRedirectProp = System.getProperty("http.maxRedirects");
var maxRedirects = 20;
if (maxRedirectProp != null) {
try {
maxRedirects = Math.max(0, Integer.parseInt(maxRedirectProp));
} catch (NumberFormatException ignored) {
}
}
MAX_HTTP_REDIRECTS = maxRedirects;
}
RequestRewritingClient(
String userAgent,
Duration requestTimeout,
@@ -84,12 +104,56 @@ final class RequestRewritingClient implements HttpClient {
this.headers = headers;
}
private <T> HttpResponse<T> doSend(
HttpRequest request,
BodyHandler<T> responseBodyHandler,
HttpRequestChecker httpRequestChecker)
throws SecurityManagerException, IOException {
var redirectCount = 0;
var currentRequestUri = rewriteUri(request.uri());
var currentRequest = rewriteRequest(request, currentRequestUri);
while (true) {
httpRequestChecker.check(currentRequestUri);
var response = delegate.send(currentRequest, responseBodyHandler, httpRequestChecker);
if (!HttpUtils.isRedirectStatusCode(response.statusCode())) {
return response;
}
if (redirectCount >= MAX_HTTP_REDIRECTS) {
throw new HttpClientException(
ErrorMessages.create("httpTooManyRedirects", MAX_HTTP_REDIRECTS));
}
var location = response.headers().firstValue("Location");
if (location.isEmpty()) {
throw new HttpClientException(
ErrorMessages.create("httpRedirectNoLocation", currentRequestUri));
}
URI redirectUri;
try {
redirectUri = currentRequestUri.resolve(location.get());
} catch (IllegalArgumentException e) {
throw new HttpClientException(
ErrorMessages.create("httpRedirectInvalidUri", currentRequestUri, location.get()));
}
if (currentRequestUri.getScheme().equalsIgnoreCase("https")
&& redirectUri.getScheme().equalsIgnoreCase("http")) {
throw new HttpClientException(
ErrorMessages.create("httpRedirectCannotDowngrade", currentRequestUri, redirectUri));
}
currentRequestUri = rewriteUri(redirectUri);
currentRequest = rewriteRequest(request, currentRequestUri);
redirectCount++;
}
}
@Override
public <T> HttpResponse<T> send(HttpRequest request, BodyHandler<T> responseBodyHandler)
throws IOException {
public <T> HttpResponse<T> send(
HttpRequest request,
BodyHandler<T> responseBodyHandler,
HttpRequestChecker httpRequestChecker)
throws IOException, SecurityManagerException {
checkNotClosed(request);
try {
return delegate.send(rewriteRequest(request), responseBodyHandler);
return doSend(request, responseBodyHandler, httpRequestChecker);
} catch (IOException e) {
var rewrittenUri = rewriteUri(request.uri());
if (rewrittenUri != request.uri()) {
@@ -108,11 +172,11 @@ final class RequestRewritingClient implements HttpClient {
}
// Based on JDK 17's implementation of HttpRequest.newBuilder(HttpRequest, filter).
private HttpRequest rewriteRequest(HttpRequest original) {
private HttpRequest rewriteRequest(HttpRequest original, URI newUri) {
HttpRequest.Builder builder = HttpRequest.newBuilder();
builder
.uri(rewriteUri(original.uri()))
.uri(newUri)
.expectContinue(original.expectContinue())
.timeout(original.timeout().orElse(requestTimeout))
.version(original.version().orElse(java.net.http.HttpClient.Version.HTTP_2));
@@ -122,7 +186,7 @@ final class RequestRewritingClient implements HttpClient {
.map()
.forEach((name, values) -> values.forEach(value -> builder.header(name, value)));
var isUserAgentSet = false;
for (var header : this.getHeaders(original.uri())) {
for (var header : this.getHeaders(newUri)) {
var headerName = header.getFirst();
isUserAgentSet = isUserAgentSet || headerName.equalsIgnoreCase("user-agent");
builder.header(header.getFirst(), header.getSecond());
@@ -519,13 +519,13 @@ public final class ModuleKeys {
@Override
public ResolvedModuleKey resolve(SecurityManager securityManager)
throws IOException, SecurityManagerException {
securityManager.checkResolveModule(uri);
var httpClient = VmContext.get(null).getHttpClient();
var request = HttpRequest.newBuilder(uri).build();
var response = httpClient.send(request, BodyHandlers.ofInputStream());
var response =
httpClient.send(
request, BodyHandlers.ofInputStream(), securityManager::checkResolveModule);
try (var body = response.body()) {
HttpUtils.checkHasStatusCode200(response);
securityManager.checkResolveModule(response.uri());
String text = IoUtils.readString(body);
// intentionally use uri instead of response.uri()
return ResolvedModuleKeys.virtual(this, uri, text, true);
@@ -201,7 +201,9 @@ final class PackageResolvers {
var request = HttpRequest.newBuilder(uri).build();
HttpResponse<InputStream> response;
try {
response = httpClient.send(request, BodyHandlers.ofInputStream());
response =
httpClient.send(
request, BodyHandlers.ofInputStream(), securityManager::checkReadResource);
} catch (IOException e) {
throw new PackageLoadError(e, "ioErrorMakingHttpGet", uri, e.getMessage());
}
@@ -257,7 +257,8 @@ public final class ResourceReaders {
static final ResourceReader INSTANCE = new FileResource();
@Override
public Optional<Object> read(URI uri) throws IOException, URISyntaxException {
public Optional<Object> read(URI uri)
throws IOException, URISyntaxException, SecurityManagerException {
IoUtils.validateFileUri(uri);
// Use resolveSecurePath to get a symlink-free path verified under rootDir.
var securityManager = VmContext.get(null).getSecurityManager();
@@ -307,7 +308,7 @@ public final class ResourceReaders {
}
}
private static final class HttpResource extends UrlResource {
public static final class HttpResource extends UrlResource {
static final ResourceReader INSTANCE = new HttpResource();
@Override
@@ -326,7 +327,7 @@ public final class ResourceReaders {
}
}
private static final class HttpsResource extends UrlResource {
public static final class HttpsResource extends UrlResource {
static final ResourceReader INSTANCE = new HttpsResource();
@Override
@@ -347,11 +348,16 @@ public final class ResourceReaders {
private abstract static class UrlResource implements ResourceReader {
@Override
public Optional<Object> read(URI uri) throws IOException, URISyntaxException {
public Optional<Object> read(URI uri)
throws IOException, URISyntaxException, SecurityManagerException {
if (HttpUtils.isHttpUrl(uri)) {
var httpClient = VmContext.get(null).getHttpClient();
var vmContext = VmContext.get(null);
var securityManager = vmContext.getSecurityManager();
var httpClient = vmContext.getHttpClient();
var request = HttpRequest.newBuilder(uri).build();
var response = httpClient.send(request, BodyHandlers.ofByteArray());
var response =
httpClient.send(
request, BodyHandlers.ofByteArray(), securityManager::checkReadResource);
if (response.statusCode() == 404) return Optional.empty();
HttpUtils.checkHasStatusCode200(response);
return Optional.of(new Resource(uri, response.body()));
@@ -31,7 +31,7 @@ import org.jspecify.annotations.Nullable;
import org.pkl.core.Release;
import org.pkl.core.SecurityManager;
import org.pkl.core.SecurityManagerException;
import org.pkl.core.http.HttpClientInitException;
import org.pkl.core.http.HttpClientException;
import org.pkl.core.module.ModuleKey;
import org.pkl.core.module.ModuleKeys;
import org.pkl.core.module.ResolvedModuleKey;
@@ -198,7 +198,7 @@ public final class ModuleCache {
ModuleKey module, SecurityManager securityManager, @Nullable Node importNode) {
try {
return module.resolve(securityManager);
} catch (SecurityManagerException | PackageLoadError | HttpClientInitException e) {
} catch (SecurityManagerException | PackageLoadError | HttpClientException e) {
throw new VmExceptionBuilder().withOptionalLocation(importNode).withCause(e).build();
} catch (FileNotFoundException | NoSuchFileException e) {
var exceptionBuilder =
@@ -28,10 +28,11 @@ import org.jspecify.annotations.Nullable;
import org.pkl.core.SecurityManager;
import org.pkl.core.SecurityManagerException;
import org.pkl.core.externalreader.ExternalReaderProcessException;
import org.pkl.core.http.HttpClientInitException;
import org.pkl.core.http.HttpClientException;
import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.resource.Resource;
import org.pkl.core.resource.ResourceReader;
import org.pkl.core.resource.ResourceReaders;
import org.pkl.core.stdlib.VmObjectFactory;
public final class ResourceManager {
@@ -86,7 +87,7 @@ public final class ResourceManager {
.build();
} catch (SecurityManagerException
| PackageLoadError
| HttpClientInitException
| HttpClientException
| ExternalReaderProcessException e) {
throw new VmExceptionBuilder().withCause(e).withOptionalLocation(readNode).build();
}
@@ -98,12 +99,17 @@ public final class ResourceManager {
return resources.computeIfAbsent(
resourceUri.normalize(),
(uri) -> {
try {
securityManager.checkReadResource(uri);
} catch (SecurityManagerException e) {
throw new VmExceptionBuilder().withCause(e).withOptionalLocation(readNode).build();
}
var reader = getResourceReader(uri);
// hack: we don't want to call `checkReadResource` here for these resources because those
// readers defer to HttpClient to do the actual checks.
if (!(reader instanceof ResourceReaders.HttpResource)
&& !(reader instanceof ResourceReaders.HttpsResource)) {
try {
securityManager.checkReadResource(uri);
} catch (SecurityManagerException e) {
throw new VmExceptionBuilder().withCause(e).withOptionalLocation(readNode).build();
}
}
if (reader == null) {
throw new VmExceptionBuilder()
.withOptionalLocation(readNode)
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,17 @@ import org.pkl.core.PklBugException;
public final class HttpUtils {
private HttpUtils() {}
public static boolean isRedirectStatusCode(int statusCode) {
return switch (statusCode) {
// We can handle each of these status codes exactly the same because:
//
// 1. We don't implement any caching for HTTPS requests.
// 2. Pkl only makes GET requests.
case 301, 302, 303, 307, 308 -> true;
default -> false;
};
}
public static boolean isHttpUrl(URL url) {
var protocol = url.getProtocol();
return "https".equalsIgnoreCase(protocol) || "http".equalsIgnoreCase(protocol);
@@ -1151,3 +1151,23 @@ HTTP header value `{0}` has invalid syntax.
invalidHttpHeaderValueTooLong=\
HTTP Header value is invalid because it is longer than 4096 characters. \
Value: `{0}`
httpTooManyRedirects=\
Too many redirects, exceeded the redirect threshold ({0}).
httpRedirectNoLocation=\
Cannot follow HTTP redirect because no ''Location'' header was found.\n\
\n\
HTTP Request: `GET {0}`
httpRedirectInvalidUri=\
Cannot follow HTTP redirect because the response ''Location'' header has a malformed URI.\n\
\n\
HTTP Request: `GET {0}`\n\
Location header: `{1}`
httpRedirectCannotDowngrade=\
Cannot follow redirect from ''https:'' URL to ''http:'' URL.\
\n\
HTTP Request: `GET {0}`\n\
Redirected to: `{1}`
@@ -175,7 +175,7 @@ class LanguageSnippetTestsEngine : AbstractLanguageSnippetTestsEngine() {
.setHttpClient(
HttpClient.builder()
.setTestPort(packageServer.port)
.addCertificates(FileTestUtils.selfSignedCertificate)
.addCertificates(FileTestUtils.selfSignedCertificatePem)
.buildLazily()
)
.setPowerAssertionsEnabled(true)
@@ -287,7 +287,7 @@ abstract class AbstractNativeLanguageSnippetTestsEngine : AbstractLanguageSnippe
add("--settings")
add("pkl:settings")
add("--ca-certificates")
add(FileTestUtils.selfSignedCertificate.toString())
add(FileTestUtils.selfSignedCertificatePem.toString())
add("--test-mode")
add("--test-port")
add(packageServer.port.toString())
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,9 +28,13 @@ class DummyHttpClientTest {
val client = HttpClient.dummyClient()
val request = HttpRequest.newBuilder(URI("https://example.com")).build()
assertThrows<AssertionError> { client.send(request, HttpResponse.BodyHandlers.discarding()) }
assertThrows<AssertionError> {
client.send(request, HttpResponse.BodyHandlers.discarding(), NoopChecker)
}
assertThrows<AssertionError> { client.send(request, HttpResponse.BodyHandlers.discarding()) }
assertThrows<AssertionError> {
client.send(request, HttpResponse.BodyHandlers.discarding(), NoopChecker)
}
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,17 +15,31 @@
*/
package org.pkl.core.http
import com.github.tomakehurst.wiremock.client.WireMock.get
import com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor
import com.github.tomakehurst.wiremock.client.WireMock.matching
import com.github.tomakehurst.wiremock.client.WireMock.ok
import com.github.tomakehurst.wiremock.client.WireMock.permanentRedirect
import com.github.tomakehurst.wiremock.client.WireMock.stubFor
import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo
import com.github.tomakehurst.wiremock.client.WireMock.verify
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
import com.github.tomakehurst.wiremock.junit5.WireMockExtension
import java.net.URI
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.file.Path
import java.time.Duration
import kotlin.io.path.absolutePathString
import kotlin.io.path.createFile
import kotlin.io.path.readBytes
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatCode
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.RegisterExtension
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.test.FileTestUtils
import org.pkl.core.Release
@@ -68,14 +82,16 @@ class HttpClientTest {
@Test
fun `can load certificates from regular file`() {
assertDoesNotThrow {
HttpClient.builder().addCertificates(FileTestUtils.selfSignedCertificate).build()
HttpClient.builder().addCertificates(FileTestUtils.selfSignedCertificatePem).build()
}
}
@Test
fun `can load certificates from a byte array`() {
assertDoesNotThrow {
HttpClient.builder().addCertificates(FileTestUtils.selfSignedCertificate.readBytes()).build()
HttpClient.builder()
.addCertificates(FileTestUtils.selfSignedCertificatePem.readBytes())
.build()
}
}
@@ -83,8 +99,7 @@ class HttpClientTest {
fun `certificate file cannot be empty`(@TempDir tempDir: Path) {
val file = tempDir.resolve("certs.pem").createFile()
val e =
assertThrows<HttpClientInitException> { HttpClient.builder().addCertificates(file).build() }
val e = assertThrows<HttpClientException> { HttpClient.builder().addCertificates(file).build() }
assertThat(e).hasMessageContaining("empty")
}
@@ -112,10 +127,185 @@ class HttpClientTest {
client.close()
assertThrows<IllegalStateException> {
client.send(request, HttpResponse.BodyHandlers.discarding())
client.send(request, HttpResponse.BodyHandlers.discarding(), NoopChecker)
}
assertThrows<IllegalStateException> {
client.send(request, HttpResponse.BodyHandlers.discarding())
client.send(request, HttpResponse.BodyHandlers.discarding(), NoopChecker)
}
}
@Nested
inner class RedirectsTest {
// incorrect diagnostic
@Suppress("JUnitMalformedDeclaration")
@RegisterExtension
val wireMock: WireMockExtension =
with(WireMockExtension.newInstance()) {
configureStaticDsl(true)
options(
wireMockConfig().apply {
dynamicPort()
dynamicHttpsPort()
keystorePath(FileTestUtils.selfSignedCertificateP12.absolutePathString())
keystorePassword(FileTestUtils.selfSignedCertificatePassword)
keystoreType("PKCS12")
}
)
build()
}
@Test
fun `follows redirects`() {
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("/bar.pkl")))
stubFor(get(urlEqualTo("/bar.pkl")).willReturn(ok("bar = 1")))
val client = HttpClient.builder().build()
val request =
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker)
assert(response.body() == "bar = 1")
verify(getRequestedFor(urlEqualTo("/foo.pkl")))
verify(getRequestedFor(urlEqualTo("/bar.pkl")))
}
@Test
fun `preserves configured headers across redirects`() {
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("/bar.pkl")))
stubFor(get(urlEqualTo("/bar.pkl")).willReturn(ok("bar = 1")))
val client =
HttpClient.builder().addHeaders("**", mapOf("x-foo" to listOf("foo value"))).build()
val request =
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker)
assert(response.body() == "bar = 1")
verify(getRequestedFor(urlEqualTo("/foo.pkl")).withHeader("x-foo", matching("foo value")))
verify(getRequestedFor(urlEqualTo("/bar.pkl")).withHeader("x-foo", matching("foo value")))
}
@Test
fun `respects configured rewrites across redirects`() {
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("/orig/bar.pkl")))
stubFor(get(urlEqualTo("/rewritten/bar.pkl")).willReturn(ok()))
val client =
HttpClient.builder()
.addRewrite(
URI("${wireMock.runtimeInfo.httpBaseUrl}/orig/"),
URI("${wireMock.runtimeInfo.httpBaseUrl}/rewritten/"),
)
.build()
val request =
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker)
verify(getRequestedFor(urlEqualTo("/foo.pkl")))
verify(getRequestedFor(urlEqualTo("/rewritten/bar.pkl")))
}
@Test
fun `cannot downgrade HTTPS to HTTP`() {
stubFor(
get(urlEqualTo("/foo.pkl"))
.willReturn(permanentRedirect("${wireMock.runtimeInfo.httpBaseUrl}/bar.pkl"))
)
val client =
HttpClient.builder()
.addCertificates(FileTestUtils.selfSignedCertificatePem)
.addHeaders("**", mapOf("x-foo" to listOf("foo value")))
.build()
val request =
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpsBaseUrl}/foo.pkl")).build()
assertThatCode { client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker) }
.hasMessageContaining("Cannot follow redirect from 'https:' URL to 'http:' URL")
}
@Test
fun `can upgrade HTTP to HTTPS`() {
stubFor(
get(urlEqualTo("/foo.pkl"))
.willReturn(permanentRedirect("${wireMock.runtimeInfo.httpsBaseUrl}/bar.pkl"))
)
stubFor(get(urlEqualTo("/bar.pkl")).willReturn(ok("hello")))
val client =
HttpClient.builder()
.addCertificates(FileTestUtils.selfSignedCertificatePem)
.addHeaders("**", mapOf("x-foo" to listOf("foo value")))
.build()
val request =
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
val response = client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker)
assertThat(response.body()).isEqualTo("hello")
}
@Test
fun `infinite redirects fail with VmException`() {
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("/bar.pkl")))
stubFor(get(urlEqualTo("/bar.pkl")).willReturn(permanentRedirect("/foo.pkl")))
val client = HttpClient.builder().build()
val request =
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
assertThatCode { client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker) }
.hasMessageContaining("Too many redirects")
verify(getRequestedFor(urlEqualTo("/foo.pkl")))
verify(getRequestedFor(urlEqualTo("/bar.pkl")))
}
@Test
fun `invalid redirect URI fails with VmException`() {
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("http://not a valid url/")))
val client = HttpClient.builder().build()
val request =
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
assertThatCode { client.send(request, HttpResponse.BodyHandlers.ofString(), NoopChecker) }
.hasMessageContaining(
"""
Cannot follow HTTP redirect because the response Location header has a malformed URI.
"""
.trimIndent()
)
verify(getRequestedFor(urlEqualTo("/foo.pkl")))
}
@Test
fun `checks each URL before making a request`() {
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("/bar.pkl")))
stubFor(get(urlEqualTo("/bar.pkl")).willReturn(permanentRedirect("/qux.pkl")))
stubFor(get(urlEqualTo("/qux.pkl")).willReturn(ok()))
val checkedUrls = mutableListOf<URI>()
val checker = HttpClient.HttpRequestChecker { uri -> checkedUrls.add(uri) }
val client = HttpClient.builder().build()
val request =
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
client.send(request, HttpResponse.BodyHandlers.ofString(), checker)
assertThat(checkedUrls).hasSize(3)
assertThat(checkedUrls)
.usingRecursiveComparison()
.isEqualTo(
listOf(
URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl"),
URI("${wireMock.runtimeInfo.httpBaseUrl}/bar.pkl"),
URI("${wireMock.runtimeInfo.httpBaseUrl}/qux.pkl"),
)
)
}
@Test
fun `redirects only carry their specifically configured headers`() {
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(permanentRedirect("/bar.pkl")))
stubFor(get(urlEqualTo("/bar.pkl")).willReturn(ok()))
val request =
HttpRequest.newBuilder(URI("${wireMock.runtimeInfo.httpBaseUrl}/foo.pkl")).build()
val client =
with(HttpClient.builder()) {
addHeaders("**/foo.pkl", mapOf("x-foo" to listOf("foo value")))
addHeaders("**/bar.pkl", mapOf("x-bar" to listOf("bar value")))
build()
}
client.send(request, HttpResponse.BodyHandlers.discarding(), NoopChecker)
verify(getRequestedFor(urlEqualTo("/foo.pkl")).withHeader("x-foo", matching("foo value")))
verify(getRequestedFor(urlEqualTo("/bar.pkl")).withoutHeader("x-foo"))
verify(getRequestedFor(urlEqualTo("/bar.pkl")).withHeader("x-bar", matching("bar value")))
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,7 +33,9 @@ class LazyHttpClientTest {
val client = HttpClient.builder().addCertificates(certFile).buildLazily()
val request = HttpRequest.newBuilder(URI("https://example.com")).build()
assertThrows<HttpClientInitException> { client.send(request, BodyHandlers.discarding()) }
assertThrows<HttpClientException> {
client.send(request, BodyHandlers.discarding(), NoopChecker)
}
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ class RequestCapturingClient : HttpClient {
override fun <T : Any> send(
request: HttpRequest,
responseBodyHandler: HttpResponse.BodyHandler<T>,
httpRequestChecker: HttpClient.HttpRequestChecker,
): HttpResponse<T> {
this.request = request
return FakeHttpResponse()
@@ -45,7 +45,7 @@ class RequestRewritingClientTest {
@Test
fun `fills in missing User-Agent header`() {
client.send(exampleRequest, BodyHandlers.discarding())
client.send(exampleRequest, BodyHandlers.discarding(), NoopChecker)
assertThatList(captured.request.headers().allValues("User-Agent")).containsOnly("Pkl")
}
@@ -61,7 +61,7 @@ class RequestRewritingClientTest {
mapOf(URI("https://foo/") to URI("https://bar/")),
mapOf(IoUtils.doubleStarGlob to mapOf("User-Agent" to listOf("My-User-Agent"))),
)
client.send(exampleRequest, BodyHandlers.discarding())
client.send(exampleRequest, BodyHandlers.discarding(), NoopChecker)
assertThatList(captured.request.headers().allValues("User-Agent")).containsOnly("My-User-Agent")
}
@@ -73,14 +73,14 @@ class RequestRewritingClientTest {
.header("User-Agent", "Agent 2")
.build()
client.send(request, BodyHandlers.discarding())
client.send(request, BodyHandlers.discarding(), NoopChecker)
assertThatList(captured.request.headers().allValues("User-Agent")).containsOnly("Pkl")
}
@Test
fun `fills in missing request timeout`() {
client.send(exampleRequest, BodyHandlers.discarding())
client.send(exampleRequest, BodyHandlers.discarding(), NoopChecker)
assertThat(captured.request.timeout()).hasValue(Duration.ofSeconds(42))
}
@@ -89,14 +89,14 @@ class RequestRewritingClientTest {
fun `leaves existing request timeout intact`() {
val request = HttpRequest.newBuilder(exampleUri).timeout(Duration.ofMinutes(33)).build()
client.send(request, BodyHandlers.discarding())
client.send(request, BodyHandlers.discarding(), NoopChecker)
assertThat(captured.request.timeout()).hasValue(Duration.ofMinutes(33))
}
@Test
fun `fills in missing HTTP version`() {
client.send(exampleRequest, BodyHandlers.discarding())
client.send(exampleRequest, BodyHandlers.discarding(), NoopChecker)
assertThat(captured.request.version()).hasValue(JdkHttpClient.Version.HTTP_2)
}
@@ -105,7 +105,7 @@ class RequestRewritingClientTest {
fun `leaves existing HTTP version intact`() {
val request = HttpRequest.newBuilder(exampleUri).version(JdkHttpClient.Version.HTTP_1_1).build()
client.send(request, BodyHandlers.discarding())
client.send(request, BodyHandlers.discarding(), NoopChecker)
assertThat(captured.request.version()).hasValue(JdkHttpClient.Version.HTTP_1_1)
}
@@ -114,7 +114,7 @@ class RequestRewritingClientTest {
fun `leaves default method intact`() {
val request = HttpRequest.newBuilder(exampleUri).build()
client.send(request, BodyHandlers.discarding())
client.send(request, BodyHandlers.discarding(), NoopChecker)
assertThat(captured.request.method()).isEqualTo("GET")
}
@@ -123,7 +123,7 @@ class RequestRewritingClientTest {
fun `leaves explicit method intact`() {
val request = HttpRequest.newBuilder(exampleUri).DELETE().build()
client.send(request, BodyHandlers.discarding())
client.send(request, BodyHandlers.discarding(), NoopChecker)
assertThat(captured.request.method()).isEqualTo("DELETE")
}
@@ -133,7 +133,7 @@ class RequestRewritingClientTest {
val publisher = BodyPublishers.ofString("body")
val request = HttpRequest.newBuilder(exampleUri).PUT(publisher).build()
client.send(request, BodyHandlers.discarding())
client.send(request, BodyHandlers.discarding(), NoopChecker)
assertThat(captured.request.bodyPublisher().get()).isSameAs(publisher)
}
@@ -145,7 +145,7 @@ class RequestRewritingClientTest {
RequestRewritingClient("Pkl", Duration.ofSeconds(42), 5000, captured, mapOf(), mapOf())
val request = HttpRequest.newBuilder(URI("https://example.com:0")).build()
client.send(request, BodyHandlers.discarding())
client.send(request, BodyHandlers.discarding(), NoopChecker)
assertThat(captured.request.uri().port).isEqualTo(5000)
}
@@ -154,7 +154,7 @@ class RequestRewritingClientTest {
fun `leaves port 0 intact if no test port is set`() {
val request = HttpRequest.newBuilder(URI("https://example.com:0")).build()
client.send(request, BodyHandlers.discarding())
client.send(request, BodyHandlers.discarding(), NoopChecker)
assertThat(captured.request.uri().port).isEqualTo(0)
}
@@ -344,7 +344,7 @@ class RequestRewritingClientTest {
val captured = RequestCapturingClient()
val client = RequestRewritingClient("Pkl", Duration.ofSeconds(42), -1, captured, rules, mapOf())
val request = HttpRequest.newBuilder(URI(uri)).build()
client.send(request, BodyHandlers.discarding())
client.send(request, BodyHandlers.discarding(), NoopChecker)
return captured.request.uri().toString()
}
@@ -366,7 +366,7 @@ class RequestRewritingClientTest {
)
val request = HttpRequest.newBuilder(URI("https://example.com/foo/bar")).build()
client.send(request, BodyHandlers.discarding())
client.send(request, BodyHandlers.discarding(), NoopChecker)
assertThatList(captured.request.headers().allValues("x-one")).containsExactly("one")
assertThatList(captured.request.headers().allValues("x-two")).containsExactly("two-a", "two-b")
@@ -389,7 +389,7 @@ class RequestRewritingClientTest {
)
val request = HttpRequest.newBuilder(URI("https://example.com/foo/bar")).build()
client.send(request, BodyHandlers.discarding())
client.send(request, BodyHandlers.discarding(), NoopChecker)
assertThat(captured.request.headers().firstValue("x-foo")).isEmpty
assertThat(captured.request.headers().firstValue("x-bar")).isEmpty
@@ -413,7 +413,7 @@ class RequestRewritingClientTest {
val request =
HttpRequest.newBuilder(URI("https://example.com/foo/bar")).header("x-foo", "request").build()
client.send(request, BodyHandlers.discarding())
client.send(request, BodyHandlers.discarding(), NoopChecker)
assertThatList(captured.request.headers().allValues("x-foo"))
.containsExactly("request", "rule-a", "rule-b")
@@ -436,7 +436,7 @@ class RequestRewritingClientTest {
)
val request = HttpRequest.newBuilder(URI("https://example.com/foo/bar")).build()
client.send(request, BodyHandlers.discarding())
client.send(request, BodyHandlers.discarding(), NoopChecker)
assertThatList(captured.request.headers().allValues("user-agent"))
.containsExactly("My User Agent")
@@ -28,3 +28,7 @@ fun HttpClient.getConfiguredSettings(): HttpSettings {
val requestRewritingClient = this.orCreateClient as RequestRewritingClient
return HttpSettings(requestRewritingClient.headers, requestRewritingClient.rewritesMap)
}
object NoopChecker : HttpClient.HttpRequestChecker {
override fun check(uri: URI) {}
}
@@ -49,7 +49,7 @@ class PackageResolversTest {
val httpClient: HttpClient by lazy {
HttpClient.builder()
.addCertificates(FileTestUtils.selfSignedCertificate)
.addCertificates(FileTestUtils.selfSignedCertificatePem)
.setTestPort(packageServer.port)
.build()
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -41,7 +41,7 @@ class ProjectDependenciesResolverTest {
val httpClient: HttpClient by lazy {
HttpClient.builder()
.addCertificates(FileTestUtils.selfSignedCertificate)
.addCertificates(FileTestUtils.selfSignedCertificatePem)
.setTestPort(packageServer.port)
.build()
}
@@ -227,7 +227,7 @@ class ProjectTest {
val project = Project.loadFromPath(projectDir.resolve("PklProject"))
val httpClient =
HttpClient.builder()
.addCertificates(FileTestUtils.selfSignedCertificate)
.addCertificates(FileTestUtils.selfSignedCertificatePem)
.setTestPort(server.port)
.build()
val evaluator =
@@ -557,7 +557,7 @@ class EmbeddedExecutorTest {
allowedModules("file:", "package:", "https:")
allowedResources("prop:", "package:", "https:")
moduleCacheDir(cacheDir)
certificateFiles(FileTestUtils.selfSignedCertificate)
certificateFiles(FileTestUtils.selfSignedCertificatePem)
testPort(server.port)
}
}
+1 -1
View File
@@ -237,7 +237,7 @@ class Http {
/// In the following example, an original request for `https://pkg.pkl-lang.org/my/pkg@1.0.0` is
/// replaced with `https://my.internal.mirror/my/pkg@1.0.0`.
///
/// This does not affect `3XX` status code redirect following.
/// This also affects `3XX` status code redirect following.
///
/// Example:
///