Improve HTTP headers logic (#1584)

* Relax forbidden headers constraints
  - remove restriction on browser-related headers
- allow any glob pattern (no need to end with `/` or `*`, because glob
patterns already require users to explicitly declare prefix matches if
that's the intention)
* Replace `List<Pair<, ...>>`; use `Map<String, ...>` instead
* Use glob pattern strings as an API throughout, instead of `Pattern`
(e.g. in `HttpClientBuilder`)
* Add HTTP headers to message passing API
* Add HTTP headers to executor API (introduces `ExecutorSpiOptions4`)
* Add tests for Gradle, CLI, and pkl-executor invocations
* Improve documentation
* Add `isGlobPattern` API to class `String` for in-language validation
of http headers
* Behavior change: make sure explicitly configured `User-Agent` in
`HttpClientBuilder` can be shadowed by headers (allows users to set
`--http-header "**=User-Agent: My User Agent"` and for this to be the
only user agent).

CC @kyokuping
This commit is contained in:
Daniel Chao
2026-05-21 20:07:06 -07:00
committed by GitHub
parent 87ea28260b
commit 8e2e5e4ba8
48 changed files with 1067 additions and 222 deletions
+1
View File
@@ -39,6 +39,7 @@ dependencies {
testImplementation(projects.pklCommonsTest)
testImplementation(projects.pklCore)
testImplementation(libs.slf4jSimple)
testImplementation(libs.wiremock)
}
// TODO why is this needed? Without this, we get error:
@@ -25,6 +25,7 @@ import org.jspecify.annotations.Nullable;
import org.pkl.executor.spi.v1.ExecutorSpiOptions;
import org.pkl.executor.spi.v1.ExecutorSpiOptions2;
import org.pkl.executor.spi.v1.ExecutorSpiOptions3;
import org.pkl.executor.spi.v1.ExecutorSpiOptions4;
/**
* Options for {@link Executor#evaluatePath}.
@@ -58,6 +59,8 @@ public final class ExecutorOptions {
private final Map<URI, URI> httpRewrites;
private final Map<String, Map<String, List<String>>> httpHeaders;
private final int testPort; // -1 means disabled
private final int spiOptionsVersion; // -1 means use latest
@@ -92,6 +95,7 @@ public final class ExecutorOptions {
private Map<URI, URI> httpRewrites = Map.of();
private int testPort = -1; // -1 means disabled
private int spiOptionsVersion = -1; // -1 means use latest
private Map<String, Map<String, List<String>>> httpHeaders = Map.of();
private Builder() {}
@@ -215,6 +219,18 @@ public final class ExecutorOptions {
return this;
}
/**
* API equivalent of the {@code --http-header} CLI option.
*
* <p>This option is ignored on Pkl 0.31 and older.
*
* @since 0.32.0
*/
public Builder httpHeaders(Map<String, Map<String, List<String>>> httpHeaders) {
this.httpHeaders = httpHeaders;
return this;
}
/** Internal test option. -1 means disabled. */
Builder testPort(int testPort) {
this.testPort = testPort;
@@ -242,6 +258,7 @@ public final class ExecutorOptions {
certificateFiles,
certificateBytes,
httpRewrites,
httpHeaders,
testPort,
spiOptionsVersion);
}
@@ -291,6 +308,7 @@ public final class ExecutorOptions {
List.of(),
List.of(),
Map.of(),
Map.of(),
-1,
-1);
}
@@ -309,6 +327,7 @@ public final class ExecutorOptions {
List<Path> certificateFiles,
List<byte[]> certificateBytes,
Map<URI, URI> httpRewrites,
Map<String, Map<String, List<String>>> httpHeaders,
int testPort,
int spiOptionsVersion) {
@@ -325,6 +344,7 @@ public final class ExecutorOptions {
this.certificateFiles = List.copyOf(certificateFiles);
this.certificateBytes = List.copyOf(certificateBytes);
this.httpRewrites = Map.copyOf(httpRewrites);
this.httpHeaders = Map.copyOf(httpHeaders);
this.testPort = testPort;
this.spiOptionsVersion = spiOptionsVersion;
}
@@ -425,6 +445,7 @@ public final class ExecutorOptions {
&& Objects.equals(certificateFiles, other.certificateFiles)
&& Objects.equals(certificateBytes, other.certificateBytes)
&& Objects.equals(httpRewrites, other.httpRewrites)
&& Objects.equals(httpHeaders, other.httpHeaders)
&& testPort == other.testPort
&& spiOptionsVersion == other.spiOptionsVersion;
}
@@ -445,6 +466,7 @@ public final class ExecutorOptions {
certificateFiles,
certificateBytes,
httpRewrites,
httpHeaders,
testPort,
spiOptionsVersion);
}
@@ -478,6 +500,8 @@ public final class ExecutorOptions {
+ certificateBytes
+ ", httpRewrites="
+ httpRewrites
+ ", httpHeaders="
+ httpHeaders
+ ", testPort="
+ testPort
+ ", spiOptionsVersion="
@@ -487,7 +511,24 @@ public final class ExecutorOptions {
ExecutorSpiOptions toSpiOptions() {
return switch (spiOptionsVersion) {
case -1, 3 ->
case -1, 4 ->
new ExecutorSpiOptions4(
allowedModules,
allowedResources,
environmentVariables,
externalProperties,
modulePath,
rootDir,
timeout,
outputFormat,
moduleCacheDir,
projectDir,
certificateFiles,
certificateBytes,
testPort,
httpRewrites,
httpHeaders);
case 3 ->
new ExecutorSpiOptions3(
allowedModules,
allowedResources,
@@ -0,0 +1,66 @@
/*
* Copyright © 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.
* You may obtain a copy of the License at
*
* https://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 org.pkl.executor.spi.v1;
import java.net.URI;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
public class ExecutorSpiOptions4 extends ExecutorSpiOptions3 {
private final Map<String, Map<String, List<String>>> httpHeaders;
public ExecutorSpiOptions4(
List<String> allowedModules,
List<String> allowedResources,
Map<String, String> environmentVariables,
Map<String, String> externalProperties,
List<Path> modulePath,
@Nullable Path rootDir,
@Nullable Duration timeout,
@Nullable String outputFormat,
@Nullable Path moduleCacheDir,
@Nullable Path projectDir,
List<Path> certificateFiles,
List<byte[]> certificateBytes,
int testPort,
Map<URI, URI> httpRewrites,
Map<String, Map<String, List<String>>> httpHeaders) {
super(
allowedModules,
allowedResources,
environmentVariables,
externalProperties,
modulePath,
rootDir,
timeout,
outputFormat,
moduleCacheDir,
projectDir,
certificateFiles,
certificateBytes,
testPort,
httpRewrites);
this.httpHeaders = httpHeaders;
}
public Map<String, Map<String, List<String>>> getHttpHeaders() {
return httpHeaders;
}
}
@@ -15,6 +15,15 @@
*/
package org.pkl.executor
import com.github.tomakehurst.wiremock.client.WireMock.equalTo
import com.github.tomakehurst.wiremock.client.WireMock.get
import com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor
import com.github.tomakehurst.wiremock.client.WireMock.ok
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.junit5.WireMockRuntimeInfo
import com.github.tomakehurst.wiremock.junit5.WireMockTest
import java.io.File
import java.net.URI
import java.nio.file.Files
@@ -39,6 +48,7 @@ import org.pkl.commons.test.PackageServer
import org.pkl.commons.toPath
import org.pkl.core.Release
@WireMockTest
class EmbeddedExecutorTest {
/**
* An executor that uses a particular combination of ExecutorSpiOptions version, pkl-executor
@@ -139,7 +149,7 @@ class EmbeddedExecutorTest {
.apply { if (!exists()) missingTestFixture() }
}
// a Pkl distribution that supports ExecutorSpiOptions up to v3
// a Pkl distribution that supports ExecutorSpiOptions up to v4
private val pklDistribution2: Path by lazy {
FileTestUtils.rootProjectDir
.resolve(
@@ -590,6 +600,30 @@ class EmbeddedExecutorTest {
)
}
@Test
fun `http headers option`(@TempDir tempDir: Path, wwRuntimeInfo: WireMockRuntimeInfo) {
stubFor(get(urlEqualTo("/foo.pkl")).willReturn(ok("foo = 1")))
val pklFile =
tempDir.resolve("test.pkl").also {
it.writeText(
"""
@ModuleInfo { minPklVersion = "0.32.0" }
module test
res = import("${wwRuntimeInfo.httpBaseUrl}/foo.pkl")
"""
.trimIndent()
)
}
currentExecutor.evaluatePath(pklFile) {
allowedModules("file:", "https:", "http:")
allowedResources("prop:")
httpHeaders(mapOf("**" to mapOf("X-Foo" to listOf("Foo"))))
}
verify(getRequestedFor(urlEqualTo("/foo.pkl")).withHeader("X-Foo", equalTo("Foo")))
}
@ParameterizedTest
@MethodSource("getCompatibleTestExecutors")
@DisabledOnOs(OS.WINDOWS, disabledReason = "Can't populate legacy cache dir on Windows")