Skip to content

Commit ba71264

Browse files
fix(typescript,java): honor env and client-default on plain global headers (#17243)
Co-authored-by: cade.sarkin <cade.sarkin@postman.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 137df8a commit ba71264

162 files changed

Lines changed: 14375 additions & 81 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
- summary: |
2+
Global headers now honor their `env` and `client-default` values. The
3+
builder initializes the header from the environment variable when set,
4+
otherwise the client default, and an explicitly provided value still takes
5+
precedence. Previously a global header was only sent when the caller set it
6+
explicitly.
7+
type: fix

generators/java/sdk/src/main/java/com/fern/java/client/generators/AbstractRootClientGenerator.java

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -566,12 +566,15 @@ public Boolean _visitUnknown(Object unknownType) {
566566

567567
if (hasCustomHeaders) {
568568
generatorContext.getIr().getHeaders().forEach(httpHeader -> {
569-
authSchemeHandler.visitNonAuthHeader(HeaderAuthScheme.builder()
570-
.key(AuthSchemeKey.of(NameUtils.getWireValue(httpHeader.getName())))
571-
.name(httpHeader.getName())
572-
.valueType(httpHeader.getValueType())
573-
.docs(httpHeader.getDocs())
574-
.build());
569+
authSchemeHandler.visitNonAuthHeader(
570+
HeaderAuthScheme.builder()
571+
.key(AuthSchemeKey.of(NameUtils.getWireValue(httpHeader.getName())))
572+
.name(httpHeader.getName())
573+
.valueType(httpHeader.getValueType())
574+
.headerEnvVar(httpHeader.getEnv().map(EnvironmentVariable::of))
575+
.docs(httpHeader.getDocs())
576+
.build(),
577+
httpHeader.getClientDefault());
575578
});
576579
}
577580

@@ -2568,17 +2571,26 @@ private OAuthCustomProperty(String name, TypeName type) {
25682571
}
25692572

25702573
public Void visitNonAuthHeader(HeaderAuthScheme header) {
2571-
return visitHeaderBase(header, false);
2574+
return visitNonAuthHeader(header, Optional.empty());
2575+
}
2576+
2577+
public Void visitNonAuthHeader(HeaderAuthScheme header, Optional<Literal> clientDefault) {
2578+
return visitHeaderBase(header, false, clientDefault);
25722579
}
25732580

25742581
public Void visitHeaderBase(HeaderAuthScheme header, Boolean respectMandatoryAuth) {
2582+
return visitHeaderBase(header, respectMandatoryAuth, Optional.empty());
2583+
}
2584+
2585+
public Void visitHeaderBase(
2586+
HeaderAuthScheme header, Boolean respectMandatoryAuth, Optional<Literal> clientDefault) {
25752587
String fieldName =
25762588
NameUtils.getName(header.getName()).getCamelCase().getSafeName();
25772589
// Never not create a setter or a null check if it's a literal
25782590
if ((respectMandatoryAuth && isMandatory)
25792591
|| !(header.getValueType().isContainer()
25802592
&& header.getValueType().getContainer().get().isLiteral())) {
2581-
createSetter(fieldName, header.getHeaderEnvVar(), Optional.empty());
2593+
createSetter(fieldName, header.getHeaderEnvVar(), Optional.empty(), Optional.empty(), clientDefault);
25822594
boolean skipValidation = generatorContext.isEndpointSecurity() && respectMandatoryAuth;
25832595
if (!skipValidation
25842596
&& ((respectMandatoryAuth && isMandatory)
@@ -2676,6 +2688,15 @@ private void createSetter(
26762688
Optional<EnvironmentVariable> environmentVariable,
26772689
Optional<Literal> literal,
26782690
Optional<TypeName> customType) {
2691+
createSetter(fieldName, environmentVariable, literal, customType, Optional.empty());
2692+
}
2693+
2694+
private void createSetter(
2695+
String fieldName,
2696+
Optional<EnvironmentVariable> environmentVariable,
2697+
Optional<Literal> literal,
2698+
Optional<TypeName> customType,
2699+
Optional<Literal> clientDefault) {
26792700
// Skip if already created to prevent duplicate fields/methods
26802701
if (createdFields.contains(fieldName)) {
26812702
return;
@@ -2684,8 +2705,21 @@ private void createSetter(
26842705

26852706
TypeName fieldType = customType.orElse(ClassName.get(String.class));
26862707
FieldSpec.Builder field = FieldSpec.builder(fieldType, fieldName).addModifiers(Modifier.PRIVATE);
2708+
Optional<String> clientDefaultValue = clientDefault.map(AbstractRootClientGenerator::literalToString);
26872709
if (environmentVariable.isPresent()) {
2688-
field.initializer("System.getenv($S)", environmentVariable.get().get());
2710+
if (clientDefaultValue.isPresent()) {
2711+
// Fall back to the client default when the environment variable is not set.
2712+
field.initializer(
2713+
"$T.ofNullable(System.getenv($S)).orElse($S)",
2714+
Optional.class,
2715+
environmentVariable.get().get(),
2716+
clientDefaultValue.get());
2717+
} else {
2718+
field.initializer(
2719+
"System.getenv($S)", environmentVariable.get().get());
2720+
}
2721+
} else if (clientDefaultValue.isPresent()) {
2722+
field.initializer("$S", clientDefaultValue.get());
26892723
} else if (literal.isPresent()) {
26902724
literal.get().visit(new Literal.Visitor<Void>() {
26912725
@Override
@@ -2939,4 +2973,23 @@ public Void _visitUnknown(Object unknownType) {
29392973
throw new RuntimeException("Encountered unknown auth scheme");
29402974
}
29412975
}
2976+
2977+
private static String literalToString(Literal literal) {
2978+
return literal.visit(new Literal.Visitor<String>() {
2979+
@Override
2980+
public String visitString(String string) {
2981+
return string;
2982+
}
2983+
2984+
@Override
2985+
public String visitBoolean(boolean boolean_) {
2986+
return Boolean.toString(boolean_);
2987+
}
2988+
2989+
@Override
2990+
public String _visitUnknown(Object unknownType) {
2991+
return null;
2992+
}
2993+
});
2994+
}
29422995
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- summary: |
2+
Global headers declared with an `env` now fall back to that environment
3+
variable at runtime. The resolution order for a global header is the
4+
explicit option, then the environment variable, then the client default.
5+
type: fix

generators/typescript/sdk/client-class-generator/src/BaseClientTypeGenerator.ts

Lines changed: 74 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -661,80 +661,14 @@ function withNoOpAuthProvider<T extends BaseClientOptions = BaseClientOptions>(
661661

662662
let value: ts.Expression;
663663
if (literalValue != null) {
664-
if (typeof literalValue === "boolean") {
665-
const booleanLiteral = literalValue ? ts.factory.createTrue() : ts.factory.createFalse();
666-
value = ts.factory.createCallExpression(
667-
ts.factory.createPropertyAccessExpression(
668-
ts.factory.createParenthesizedExpression(
669-
ts.factory.createBinaryExpression(
670-
ts.factory.createPropertyAccessChain(
671-
ts.factory.createIdentifier(OPTIONS_PARAMETER_NAME),
672-
ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
673-
ts.factory.createIdentifier(headerName)
674-
),
675-
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
676-
booleanLiteral
677-
)
678-
),
679-
ts.factory.createIdentifier("toString")
680-
),
681-
undefined,
682-
[]
683-
);
684-
} else {
685-
value = ts.factory.createBinaryExpression(
686-
ts.factory.createPropertyAccessChain(
687-
ts.factory.createIdentifier(OPTIONS_PARAMETER_NAME),
688-
ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
689-
ts.factory.createIdentifier(headerName)
690-
),
691-
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
692-
ts.factory.createStringLiteral(literalValue.toString())
693-
);
694-
}
664+
value = this.buildRootHeaderValue({ headerName, envVar: undefined, fallback: literalValue });
695665
} else {
696666
const clientDefaultVal = getClientDefaultValue(header.clientDefault);
697-
if (clientDefaultVal != null && !typeContainsNullable(header.valueType, context)) {
698-
if (typeof clientDefaultVal === "boolean") {
699-
const booleanLiteral = clientDefaultVal
700-
? ts.factory.createTrue()
701-
: ts.factory.createFalse();
702-
value = ts.factory.createCallExpression(
703-
ts.factory.createPropertyAccessExpression(
704-
ts.factory.createParenthesizedExpression(
705-
ts.factory.createBinaryExpression(
706-
ts.factory.createPropertyAccessChain(
707-
ts.factory.createIdentifier(OPTIONS_PARAMETER_NAME),
708-
ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
709-
ts.factory.createIdentifier(headerName)
710-
),
711-
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
712-
booleanLiteral
713-
)
714-
),
715-
ts.factory.createIdentifier("toString")
716-
),
717-
undefined,
718-
[]
719-
);
720-
} else {
721-
value = ts.factory.createBinaryExpression(
722-
ts.factory.createPropertyAccessChain(
723-
ts.factory.createIdentifier(OPTIONS_PARAMETER_NAME),
724-
ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
725-
ts.factory.createIdentifier(headerName)
726-
),
727-
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
728-
ts.factory.createStringLiteral(clientDefaultVal.toString())
729-
);
730-
}
731-
} else {
732-
value = ts.factory.createPropertyAccessChain(
733-
ts.factory.createIdentifier(OPTIONS_PARAMETER_NAME),
734-
ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
735-
ts.factory.createIdentifier(this.getOptionKeyForHeader(header, context))
736-
);
737-
}
667+
const fallback =
668+
clientDefaultVal != null && !typeContainsNullable(header.valueType, context)
669+
? clientDefaultVal
670+
: undefined;
671+
value = this.buildRootHeaderValue({ headerName, envVar: header.env, fallback });
738672
}
739673

740674
return {
@@ -776,6 +710,74 @@ function withNoOpAuthProvider<T extends BaseClientOptions = BaseClientOptions>(
776710
return headers;
777711
}
778712

713+
/**
714+
* Builds the value expression for a root (global) header, coalescing in
715+
* precedence order: the client option, then the environment variable
716+
* fallback (when the header declares an `env`), then the client default.
717+
*/
718+
private buildRootHeaderValue({
719+
headerName,
720+
envVar,
721+
fallback
722+
}: {
723+
headerName: string;
724+
envVar: string | undefined;
725+
fallback: string | boolean | undefined;
726+
}): ts.Expression {
727+
const operands: ts.Expression[] = [
728+
ts.factory.createPropertyAccessChain(
729+
ts.factory.createIdentifier(OPTIONS_PARAMETER_NAME),
730+
ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
731+
ts.factory.createIdentifier(headerName)
732+
)
733+
];
734+
735+
if (envVar != null) {
736+
operands.push(
737+
ts.factory.createElementAccessChain(
738+
ts.factory.createPropertyAccessExpression(
739+
ts.factory.createIdentifier("process"),
740+
ts.factory.createIdentifier("env")
741+
),
742+
ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
743+
ts.factory.createStringLiteral(envVar)
744+
)
745+
);
746+
}
747+
748+
let wrapWithToString = false;
749+
if (fallback != null) {
750+
if (typeof fallback === "boolean") {
751+
wrapWithToString = true;
752+
operands.push(fallback ? ts.factory.createTrue() : ts.factory.createFalse());
753+
} else {
754+
operands.push(ts.factory.createStringLiteral(fallback.toString()));
755+
}
756+
}
757+
758+
const first = operands[0];
759+
if (operands.length === 1 && first != null) {
760+
return first;
761+
}
762+
763+
const coalesced = operands.reduce((left, right) =>
764+
ts.factory.createBinaryExpression(left, ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken), right)
765+
);
766+
767+
if (wrapWithToString) {
768+
return ts.factory.createCallExpression(
769+
ts.factory.createPropertyAccessExpression(
770+
ts.factory.createParenthesizedExpression(coalesced),
771+
ts.factory.createIdentifier("toString")
772+
),
773+
undefined,
774+
[]
775+
);
776+
}
777+
778+
return coalesced;
779+
}
780+
779781
private getOptionKeyForHeader(header: FernIr.HttpHeader, context: FileContext): string {
780782
return context.case.camelUnsafe(header.name);
781783
}

generators/typescript/sdk/client-class-generator/src/endpoints/utils/generateHeaders.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,23 @@ function getOverridableRootHeaders({
384384
ts.factory.createIdentifier(getOptionKeyForHeader(header, context))
385385
);
386386

387+
// If the header declares an env var, chain it before the client default:
388+
// requestOptions?.header ?? this._options?.header ?? process.env?.["ENV"] ?? "clientDefault"
389+
if (header.env != null) {
390+
fallbackExpr = ts.factory.createBinaryExpression(
391+
fallbackExpr,
392+
ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken),
393+
ts.factory.createElementAccessChain(
394+
ts.factory.createPropertyAccessExpression(
395+
ts.factory.createIdentifier("process"),
396+
ts.factory.createIdentifier("env")
397+
),
398+
ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
399+
ts.factory.createStringLiteral(header.env)
400+
)
401+
);
402+
}
403+
387404
// If clientDefault is set, chain it as final fallback:
388405
// requestOptions?.header ?? this._options?.header ?? "clientDefault"
389406
// Skip when the type is nullable — explicit null means "don't send the header".

seed/java-sdk/java-global-header-env/.fern/metadata.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

seed/java-sdk/java-global-header-env/.github/workflows/ci.yml

Lines changed: 65 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)