Skip to content

Commit 14085c1

Browse files
kyokupingHT154stackoverflow
authored
Add support for customizing HTTP headers (apple#1196)
This PR adds support for custom HTTP headers, introducing a `--http-header` CLI flag to accept `key=value` pairs. These headers can also be specified within the `setting.pkl` file. Closes apple#633 SPICE: apple/pkl-evolution#24 --------- Co-authored-by: Jen Basch <jbasch94@gmail.com> Co-authored-by: Islon Scherer <islonscherer@gmail.com>
1 parent fe58405 commit 14085c1

14 files changed

Lines changed: 358 additions & 18 deletions

File tree

pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliBaseOptions.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import java.nio.file.Files
2020
import java.nio.file.Path
2121
import java.time.Duration
2222
import java.util.regex.Pattern
23+
import org.pkl.core.Pair
2324
import org.pkl.core.evaluatorSettings.Color
2425
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader
2526
import org.pkl.core.evaluatorSettings.TraceMode
@@ -144,6 +145,9 @@ data class CliBaseOptions(
144145
/** URL prefixes to rewrite. */
145146
val httpRewrites: Map<URI, URI>? = null,
146147

148+
/** HTTP headers to add to the request. */
149+
val httpHeaders: List<Pair<Pattern, List<Pair<String, String>>>>? = null,
150+
147151
/** External module reader process specs */
148152
val externalModuleReaders: Map<String, ExternalReader> = mapOf(),
149153

pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/CliCommand.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
218218
cliOptions.httpRewrites ?: evaluatorSettings?.http?.rewrites ?: settings.http?.rewrites()
219219
}
220220

221+
private val httpHeaders: List<Pair<Pattern, List<Pair<String, String>>>>? by lazy {
222+
cliOptions.httpHeaders ?: project?.evaluatorSettings?.http?.headers ?: settings.http?.headers
223+
}
224+
221225
protected val externalModuleReaders: Map<String, PklEvaluatorSettings.ExternalReader> by lazy {
222226
(evaluatorSettings?.externalModuleReaders ?: emptyMap()) + cliOptions.externalModuleReaders
223227
}
@@ -277,6 +281,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
277281
setProxy(proxyAddress, noProxy ?: listOf())
278282
}
279283
httpRewrites?.let(::setRewrites)
284+
httpHeaders?.let(::setHeaders)
280285
// Lazy building significantly reduces execution time of commands that do minimal work.
281286
// However, it means that HTTP client initialization errors won't surface until an HTTP
282287
// request is made.

pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/BaseOptions.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ import java.util.regex.Pattern
3131
import org.pkl.commons.cli.CliBaseOptions
3232
import org.pkl.commons.cli.CliException
3333
import org.pkl.commons.shlex
34+
import org.pkl.core.Pair as PPair
3435
import org.pkl.core.evaluatorSettings.Color
3536
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader
3637
import org.pkl.core.evaluatorSettings.TraceMode
3738
import org.pkl.core.runtime.VmUtils
39+
import org.pkl.core.util.GlobResolver
3840
import org.pkl.core.util.IoUtils
3941

4042
@Suppress("MemberVisibilityCanBePrivate")
@@ -285,6 +287,37 @@ class BaseOptions : OptionGroup() {
285287
.multiple()
286288
.toMap()
287289

290+
val httpHeaders: List<PPair<Pattern, List<PPair<String, String>>>> by
291+
option(
292+
names = arrayOf("--http-headers"),
293+
metavar = "<url-pattern>=<header name>:<header value>",
294+
help = "HTTP header to add to the request.",
295+
)
296+
.splitPair()
297+
.transformAll { it ->
298+
val headersMap = mutableMapOf<String, MutableList<PPair<String, String>>>()
299+
300+
try {
301+
val headerRegex = Regex("""^(.+?):[ \t]*(.+)$""")
302+
for ((stringPattern, header) in it) {
303+
val (headerName, headerValue) =
304+
headerRegex.find(header)?.destructured
305+
?: fail("Header '$header' is not in 'name:value' format.")
306+
IoUtils.validateHeaderName(headerName)
307+
IoUtils.validateHeaderValue(headerValue)
308+
headersMap
309+
.computeIfAbsent(stringPattern) { mutableListOf() }
310+
.add(PPair(headerName, headerValue))
311+
}
312+
313+
headersMap.entries.map { PPair(GlobResolver.toRegexPattern(it.key), it.value) }
314+
} catch (e: IllegalArgumentException) {
315+
fail(e.message!!)
316+
} catch (e: GlobResolver.InvalidGlobPatternException) {
317+
fail(e.message!!)
318+
}
319+
}
320+
288321
val externalModuleReaders: Map<String, ExternalReader> by
289322
option(
290323
names = arrayOf("--external-module-reader"),
@@ -351,6 +384,7 @@ class BaseOptions : OptionGroup() {
351384
httpProxy = proxy,
352385
httpNoProxy = noProxy,
353386
httpRewrites = httpRewrites.ifEmpty { null },
387+
httpHeaders = httpHeaders.ifEmpty { null },
354388
externalModuleReaders = externalModuleReaders,
355389
externalResourceReaders = externalResourceReaders,
356390
traceMode = traceMode,

pkl-core/src/main/java/org/pkl/core/evaluatorSettings/PklEvaluatorSettings.java

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
2+
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818
import java.net.URI;
1919
import java.net.URISyntaxException;
2020
import java.nio.file.Path;
21+
import java.util.ArrayList;
2122
import java.util.Collections;
2223
import java.util.HashMap;
2324
import java.util.List;
@@ -27,13 +28,17 @@
2728
import java.util.function.BiFunction;
2829
import java.util.regex.Pattern;
2930
import java.util.stream.Collectors;
31+
import java.util.stream.Stream;
3032
import org.pkl.core.Duration;
3133
import org.pkl.core.PNull;
3234
import org.pkl.core.PObject;
35+
import org.pkl.core.Pair;
3336
import org.pkl.core.PklBugException;
3437
import org.pkl.core.PklException;
3538
import org.pkl.core.Value;
3639
import org.pkl.core.util.ErrorMessages;
40+
import org.pkl.core.util.GlobResolver;
41+
import org.pkl.core.util.GlobResolver.InvalidGlobPatternException;
3742
import org.pkl.core.util.Nullable;
3843

3944
/** Java version of {@code pkl.EvaluatorSettings}. */
@@ -126,8 +131,11 @@ public static PklEvaluatorSettings parse(
126131
traceMode == null ? null : TraceMode.valueOf(traceMode.toUpperCase()));
127132
}
128133

129-
public record Http(@Nullable Proxy proxy, @Nullable Map<URI, URI> rewrites) {
130-
public static final Http DEFAULT = new Http(null, Collections.emptyMap());
134+
public record Http(
135+
@Nullable Proxy proxy,
136+
@Nullable Map<URI, URI> rewrites,
137+
@Nullable List<Pair<Pattern, List<Pair<String, String>>>> headers) {
138+
public static final Http DEFAULT = new Http(null, Collections.emptyMap(), null);
131139

132140
@SuppressWarnings("unchecked")
133141
public static @Nullable Http parse(@Nullable Value input) {
@@ -136,10 +144,9 @@ public record Http(@Nullable Proxy proxy, @Nullable Map<URI, URI> rewrites) {
136144
} else if (input instanceof PObject http) {
137145
var proxy = Proxy.parse((Value) http.getProperty("proxy"));
138146
var rewrites = http.getProperty("rewrites");
139-
if (rewrites instanceof PNull) {
140-
return new Http(proxy, null);
141-
} else {
142-
var parsedRewrites = new HashMap<URI, URI>();
147+
HashMap<URI, URI> parsedRewrites = null;
148+
if (!(rewrites instanceof PNull)) {
149+
parsedRewrites = new HashMap<>();
143150
for (var entry : ((Map<String, String>) rewrites).entrySet()) {
144151
var key = entry.getKey();
145152
var value = entry.getValue();
@@ -149,8 +156,37 @@ public record Http(@Nullable Proxy proxy, @Nullable Map<URI, URI> rewrites) {
149156
throw new PklException(ErrorMessages.create("invalidUri", e.getInput()));
150157
}
151158
}
152-
return new Http(proxy, parsedRewrites);
153159
}
160+
var headerDefs = http.getProperty("headers");
161+
List<Pair<Pattern, List<Pair<String, String>>>> parsedHeaderDefs = null;
162+
if (!(headerDefs instanceof PNull)) {
163+
parsedHeaderDefs = new ArrayList<>();
164+
var headerDefsMap = (Map<String, Map<String, Object>>) headerDefs;
165+
for (var entry : headerDefsMap.entrySet()) {
166+
var stringPattern = entry.getKey();
167+
var headersMap = entry.getValue();
168+
try {
169+
var urlPattern = GlobResolver.toRegexPattern(stringPattern);
170+
var pairs =
171+
headersMap.entrySet().stream()
172+
.flatMap(
173+
header -> {
174+
var value = header.getValue();
175+
if (value instanceof List) {
176+
return ((List<String>) value)
177+
.stream().map(v -> new Pair(header.getKey(), v));
178+
} else {
179+
return Stream.of(new Pair(header.getKey(), value));
180+
}
181+
})
182+
.toList();
183+
parsedHeaderDefs.add(new Pair(urlPattern, pairs));
184+
} catch (InvalidGlobPatternException e) {
185+
throw new PklException(ErrorMessages.create("invalidUri", stringPattern));
186+
}
187+
}
188+
}
189+
return new Http(proxy, parsedRewrites, parsedHeaderDefs);
154190
} else {
155191
throw PklBugException.unreachableCode();
156192
}

pkl-core/src/main/java/org/pkl/core/http/HttpClient.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
2+
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,7 +23,9 @@
2323
import java.nio.file.Path;
2424
import java.util.List;
2525
import java.util.Map;
26+
import java.util.regex.Pattern;
2627
import javax.net.ssl.SSLContext;
28+
import org.pkl.core.Pair;
2729
import org.pkl.core.util.Nullable;
2830

2931
/**
@@ -150,6 +152,14 @@ interface Builder {
150152
*/
151153
Builder addRewrite(URI sourcePrefix, URI targetPrefix);
152154

155+
/**
156+
* Sets the HTTP headers for the request, replacing any previously configured headers.
157+
*
158+
* <p>This method clears all existing headers and replaces them with the contents of the
159+
* provided map.
160+
*/
161+
Builder setHeaders(List<Pair<Pattern, List<Pair<String, String>>>> headers);
162+
153163
/**
154164
* Creates a new {@code HttpClient} from the current state of this builder.
155165
*

pkl-core/src/main/java/org/pkl/core/http/HttpClientBuilder.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
2+
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,8 @@
2727
import java.util.List;
2828
import java.util.Map;
2929
import java.util.function.Supplier;
30+
import java.util.regex.Pattern;
31+
import org.pkl.core.Pair;
3032
import org.pkl.core.Release;
3133
import org.pkl.core.http.HttpClient.Builder;
3234

@@ -39,6 +41,7 @@ final class HttpClientBuilder implements HttpClient.Builder {
3941
private int testPort = -1;
4042
private ProxySelector proxySelector;
4143
private Map<URI, URI> rewrites = new HashMap<>();
44+
private List<Pair<Pattern, List<Pair<String, String>>>> headers = new ArrayList<>();
4245

4346
HttpClientBuilder() {
4447
var release = Release.current();
@@ -110,6 +113,12 @@ public Builder addRewrite(URI sourcePrefix, URI targetPrefix) {
110113
return this;
111114
}
112115

116+
@Override
117+
public Builder setHeaders(List<Pair<Pattern, List<Pair<String, String>>>> headers) {
118+
this.headers = headers;
119+
return this;
120+
}
121+
113122
@Override
114123
public HttpClient build() {
115124
return doBuild().get();
@@ -128,7 +137,8 @@ private Supplier<HttpClient> doBuild() {
128137
return () -> {
129138
var jdkClient =
130139
new JdkHttpClient(certificateFiles, certificateBytes, connectTimeout, proxySelector);
131-
return new RequestRewritingClient(userAgent, requestTimeout, testPort, jdkClient, rewrites);
140+
return new RequestRewritingClient(
141+
userAgent, requestTimeout, testPort, jdkClient, rewrites, headers);
132142
};
133143
}
134144
}

pkl-core/src/main/java/org/pkl/core/http/RequestRewritingClient.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
2+
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,7 +28,10 @@
2828
import java.util.Map.Entry;
2929
import java.util.Objects;
3030
import java.util.concurrent.atomic.AtomicBoolean;
31+
import java.util.regex.Pattern;
32+
import java.util.stream.Stream;
3133
import javax.annotation.concurrent.ThreadSafe;
34+
import org.pkl.core.Pair;
3235
import org.pkl.core.PklBugException;
3336
import org.pkl.core.util.HttpUtils;
3437
import org.pkl.core.util.Nullable;
@@ -54,6 +57,7 @@ final class RequestRewritingClient implements HttpClient {
5457
final int testPort;
5558
final HttpClient delegate;
5659
private final List<Entry<URI, URI>> rewrites;
60+
private final List<Pair<Pattern, List<Pair<String, String>>>> headers;
5761

5862
private final AtomicBoolean closed = new AtomicBoolean();
5963

@@ -62,7 +66,8 @@ final class RequestRewritingClient implements HttpClient {
6266
Duration requestTimeout,
6367
int testPort,
6468
HttpClient delegate,
65-
Map<URI, URI> rewrites) {
69+
Map<URI, URI> rewrites,
70+
List<Pair<Pattern, List<Pair<String, String>>>> headers) {
6671
this.userAgent = userAgent;
6772
this.requestTimeout = requestTimeout;
6873
this.testPort = testPort;
@@ -72,6 +77,7 @@ final class RequestRewritingClient implements HttpClient {
7277
.map((it) -> Map.entry(normalizeRewrite(it.getKey()), normalizeRewrite(it.getValue())))
7378
.sorted(Comparator.comparingInt((it) -> -it.getKey().toString().length()))
7479
.toList();
80+
this.headers = headers;
7581
}
7682

7783
@Override
@@ -112,6 +118,9 @@ private HttpRequest rewriteRequest(HttpRequest original) {
112118
.map()
113119
.forEach((name, values) -> values.forEach(value -> builder.header(name, value)));
114120
builder.setHeader("User-Agent", userAgent);
121+
for (var header : this.getHeaders(original.uri())) {
122+
builder.header(header.getFirst(), header.getSecond());
123+
}
115124

116125
var method = original.method();
117126
original
@@ -216,6 +225,16 @@ private URI rewriteUri(URI uri) {
216225
return ret;
217226
}
218227

228+
private List<Pair<String, String>> getHeaders(URI uri) {
229+
return headers.stream()
230+
.flatMap(
231+
rule ->
232+
rule.getFirst().asPredicate().test(uri.toString())
233+
? rule.getSecond().stream()
234+
: Stream.empty())
235+
.toList();
236+
}
237+
219238
private void checkNotClosed(HttpRequest request) {
220239
if (closed.get()) {
221240
throw new IllegalStateException(

0 commit comments

Comments
 (0)