-
Notifications
You must be signed in to change notification settings - Fork 386
Add support for customizing HTTP headers #1196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5706643
e119cab
5e62b61
d48b858
26fff36
ce945a3
f805e25
0e4078c
857cee0
72f1897
487d04c
39194ba
f0c21a0
97ee16b
6832d82
729673b
c14818c
2433bc3
e4c4754
5620d56
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,10 +31,12 @@ import java.util.regex.Pattern | |
| import org.pkl.commons.cli.CliBaseOptions | ||
| import org.pkl.commons.cli.CliException | ||
| import org.pkl.commons.shlex | ||
| import org.pkl.core.Pair as PPair | ||
| import org.pkl.core.evaluatorSettings.Color | ||
| import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader | ||
| import org.pkl.core.evaluatorSettings.TraceMode | ||
| import org.pkl.core.runtime.VmUtils | ||
| import org.pkl.core.util.GlobResolver | ||
| import org.pkl.core.util.IoUtils | ||
|
|
||
| @Suppress("MemberVisibilityCanBePrivate") | ||
|
|
@@ -285,6 +287,37 @@ class BaseOptions : OptionGroup() { | |
| .multiple() | ||
| .toMap() | ||
|
|
||
| val httpHeaders: List<PPair<Pattern, List<PPair<String, String>>>> by | ||
| option( | ||
| names = arrayOf("--http-headers"), | ||
| metavar = "<url-pattern>=<header name>:<header value>", | ||
| help = "HTTP header to add to the request.", | ||
| ) | ||
| .splitPair() | ||
| .transformAll { it -> | ||
| val headersMap = mutableMapOf<String, MutableList<PPair<String, String>>>() | ||
|
|
||
| try { | ||
| val headerRegex = Regex("""^(.+?):[ \t]*(.+)$""") | ||
| for ((stringPattern, header) in it) { | ||
| val (headerName, headerValue) = | ||
| headerRegex.find(header)?.destructured | ||
| ?: fail("Header '$header' is not in 'name:value' format.") | ||
| IoUtils.validateHeaderName(headerName) | ||
| IoUtils.validateHeaderValue(headerValue) | ||
| headersMap | ||
| .computeIfAbsent(stringPattern) { mutableListOf() } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The pattern here isn't validated by the same rule as the pattern values in |
||
| .add(PPair(headerName, headerValue)) | ||
| } | ||
|
|
||
| headersMap.entries.map { PPair(GlobResolver.toRegexPattern(it.key), it.value) } | ||
| } catch (e: IllegalArgumentException) { | ||
| fail(e.message!!) | ||
| } catch (e: GlobResolver.InvalidGlobPatternException) { | ||
| fail(e.message!!) | ||
| } | ||
| } | ||
|
|
||
| val externalModuleReaders: Map<String, ExternalReader> by | ||
| option( | ||
| names = arrayOf("--external-module-reader"), | ||
|
|
@@ -351,6 +384,7 @@ class BaseOptions : OptionGroup() { | |
| httpProxy = proxy, | ||
| httpNoProxy = noProxy, | ||
| httpRewrites = httpRewrites.ifEmpty { null }, | ||
| httpHeaders = httpHeaders.ifEmpty { null }, | ||
| externalModuleReaders = externalModuleReaders, | ||
| externalResourceReaders = externalResourceReaders, | ||
| traceMode = traceMode, | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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. | ||||||
|
|
@@ -18,6 +18,7 @@ | |||||
| import java.net.URI; | ||||||
| import java.net.URISyntaxException; | ||||||
| import java.nio.file.Path; | ||||||
| import java.util.ArrayList; | ||||||
| import java.util.Collections; | ||||||
| import java.util.HashMap; | ||||||
| import java.util.List; | ||||||
|
|
@@ -27,13 +28,17 @@ | |||||
| import java.util.function.BiFunction; | ||||||
| import java.util.regex.Pattern; | ||||||
| import java.util.stream.Collectors; | ||||||
| import java.util.stream.Stream; | ||||||
| import org.pkl.core.Duration; | ||||||
| import org.pkl.core.PNull; | ||||||
| import org.pkl.core.PObject; | ||||||
| import org.pkl.core.Pair; | ||||||
| import org.pkl.core.PklBugException; | ||||||
| import org.pkl.core.PklException; | ||||||
| import org.pkl.core.Value; | ||||||
| import org.pkl.core.util.ErrorMessages; | ||||||
| import org.pkl.core.util.GlobResolver; | ||||||
| import org.pkl.core.util.GlobResolver.InvalidGlobPatternException; | ||||||
| import org.pkl.core.util.Nullable; | ||||||
|
|
||||||
| /** Java version of {@code pkl.EvaluatorSettings}. */ | ||||||
|
|
@@ -126,8 +131,11 @@ public static PklEvaluatorSettings parse( | |||||
| traceMode == null ? null : TraceMode.valueOf(traceMode.toUpperCase())); | ||||||
| } | ||||||
|
|
||||||
| public record Http(@Nullable Proxy proxy, @Nullable Map<URI, URI> rewrites) { | ||||||
| public static final Http DEFAULT = new Http(null, Collections.emptyMap()); | ||||||
| public record Http( | ||||||
| @Nullable Proxy proxy, | ||||||
| @Nullable Map<URI, URI> rewrites, | ||||||
| @Nullable List<Pair<Pattern, List<Pair<String, String>>>> headers) { | ||||||
| public static final Http DEFAULT = new Http(null, Collections.emptyMap(), null); | ||||||
|
|
||||||
| @SuppressWarnings("unchecked") | ||||||
| public static @Nullable Http parse(@Nullable Value input) { | ||||||
|
|
@@ -136,10 +144,9 @@ public record Http(@Nullable Proxy proxy, @Nullable Map<URI, URI> rewrites) { | |||||
| } else if (input instanceof PObject http) { | ||||||
| var proxy = Proxy.parse((Value) http.getProperty("proxy")); | ||||||
| var rewrites = http.getProperty("rewrites"); | ||||||
| if (rewrites instanceof PNull) { | ||||||
| return new Http(proxy, null); | ||||||
| } else { | ||||||
| var parsedRewrites = new HashMap<URI, URI>(); | ||||||
| HashMap<URI, URI> parsedRewrites = null; | ||||||
| if (!(rewrites instanceof PNull)) { | ||||||
| parsedRewrites = new HashMap<>(); | ||||||
| for (var entry : ((Map<String, String>) rewrites).entrySet()) { | ||||||
| var key = entry.getKey(); | ||||||
| var value = entry.getValue(); | ||||||
|
|
@@ -149,8 +156,37 @@ public record Http(@Nullable Proxy proxy, @Nullable Map<URI, URI> rewrites) { | |||||
| throw new PklException(ErrorMessages.create("invalidUri", e.getInput())); | ||||||
| } | ||||||
| } | ||||||
| return new Http(proxy, parsedRewrites); | ||||||
| } | ||||||
| var headerDefs = http.getProperty("headers"); | ||||||
| List<Pair<Pattern, List<Pair<String, String>>>> parsedHeaderDefs = null; | ||||||
| if (!(headerDefs instanceof PNull)) { | ||||||
| parsedHeaderDefs = new ArrayList<>(); | ||||||
| var headerDefsMap = (Map<String, Map<String, Object>>) headerDefs; | ||||||
| for (var entry : headerDefsMap.entrySet()) { | ||||||
| var stringPattern = entry.getKey(); | ||||||
| var headersMap = entry.getValue(); | ||||||
| try { | ||||||
| var urlPattern = GlobResolver.toRegexPattern(stringPattern); | ||||||
| var pairs = | ||||||
| headersMap.entrySet().stream() | ||||||
| .flatMap( | ||||||
| header -> { | ||||||
| var value = header.getValue(); | ||||||
| if (value instanceof List) { | ||||||
| return ((List<String>) value) | ||||||
| .stream().map(v -> new Pair(header.getKey(), v)); | ||||||
| } else { | ||||||
| return Stream.of(new Pair(header.getKey(), value)); | ||||||
| } | ||||||
| }) | ||||||
| .toList(); | ||||||
| parsedHeaderDefs.add(new Pair(urlPattern, pairs)); | ||||||
| } catch (InvalidGlobPatternException e) { | ||||||
| throw new PklException(ErrorMessages.create("invalidUri", stringPattern)); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
| } | ||||||
| } | ||||||
| return new Http(proxy, parsedRewrites, parsedHeaderDefs); | ||||||
| } else { | ||||||
| throw PklBugException.unreachableCode(); | ||||||
| } | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
|
|
@@ -27,6 +27,8 @@ | |
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.function.Supplier; | ||
| import java.util.regex.Pattern; | ||
| import org.pkl.core.Pair; | ||
| import org.pkl.core.Release; | ||
| import org.pkl.core.http.HttpClient.Builder; | ||
|
|
||
|
|
@@ -39,6 +41,7 @@ final class HttpClientBuilder implements HttpClient.Builder { | |
| private int testPort = -1; | ||
| private ProxySelector proxySelector; | ||
| private Map<URI, URI> rewrites = new HashMap<>(); | ||
| private List<Pair<Pattern, List<Pair<String, String>>>> headers = new ArrayList<>(); | ||
|
|
||
| HttpClientBuilder() { | ||
| var release = Release.current(); | ||
|
|
@@ -110,6 +113,12 @@ public Builder addRewrite(URI sourcePrefix, URI targetPrefix) { | |
| return this; | ||
| } | ||
|
|
||
| @Override | ||
| public Builder setHeaders(List<Pair<Pattern, List<Pair<String, String>>>> headers) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be better if this signature matched the in-language evaluator settings. I think this should be something like: setHeaderRules(Map<String, Map<String, List<String>>> headerRules)Where the top-level map key is the glob pattern. And, I think we should also have: addHeaderRule(String globPattern, Map<String, List<String>> headers)It'd also be good to avoid using |
||
| this.headers = headers; | ||
| return this; | ||
| } | ||
|
|
||
| @Override | ||
| public HttpClient build() { | ||
| return doBuild().get(); | ||
|
|
@@ -128,7 +137,8 @@ private Supplier<HttpClient> doBuild() { | |
| return () -> { | ||
| var jdkClient = | ||
| new JdkHttpClient(certificateFiles, certificateBytes, connectTimeout, proxySelector); | ||
| return new RequestRewritingClient(userAgent, requestTimeout, testPort, jdkClient, rewrites); | ||
| return new RequestRewritingClient( | ||
| userAgent, requestTimeout, testPort, jdkClient, rewrites, headers); | ||
| }; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more of a nit, but:
List<PPair<Pattern, List<PPair<String, String>>>>is a quite generic type that doesn't convey much information. It would be better to have a properly-named record that stores the URL glob and the header name-value pairs.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree this type's gotten a bit out of hand and a record would be a welcome improvement. I'm not eager to block this PR more than necessary, so I'm fine leaving this as follow-up work.