Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
14c77ea
feat(rust): add config-driven retryStatusCodes with legacy/recommende…
iamnamananand996 Apr 29, 2026
c7f5a03
chore(rust): release 0.35.0
github-actions[bot] Apr 29, 2026
2dcfbb2
chore(rust): update rust-sdk seed (#15560)
fern-support Apr 29, 2026
0a55feb
feat(ruby): add config-driven retryStatusCodes with legacy/recommende…
iamnamananand996 Apr 29, 2026
db27e3c
chore(rust): update rust-sdk seed (#15561)
fern-support Apr 29, 2026
71061e2
chore(ruby-v2): release 1.11.0
github-actions[bot] Apr 29, 2026
7264833
feat(go): add config-driven retryStatusCodes with legacy/recommended …
iamnamananand996 Apr 29, 2026
92c4571
chore(go): release 1.39.0
github-actions[bot] Apr 29, 2026
6c3bf2d
chore(ruby): update ruby-sdk-v2 seed (#15562)
fern-support Apr 29, 2026
b033c3e
feat(swift): add config-driven retryStatusCodes with legacy/recommend…
iamnamananand996 Apr 29, 2026
6a5c3e9
chore(swift): release 0.34.0
github-actions[bot] Apr 29, 2026
f703e26
chore(go): update go-sdk seed (#15565)
fern-support Apr 29, 2026
5dc2137
chore(ruby): update ruby-sdk-v2 seed (#15566)
fern-support Apr 29, 2026
8729bff
fix(typescript): dedupe discriminant in generated SSE protocol-union …
patrickthornton Apr 29, 2026
9fd3e8d
feat(php): add config-driven retryStatusCodes with legacy/recommended…
iamnamananand996 Apr 29, 2026
96c89c7
fix(cli): make OpenAPI validation non-fatal during docs generation (#…
devin-ai-integration[bot] Apr 29, 2026
06753dc
chore(cli): release 4.107.1
github-actions[bot] Apr 29, 2026
48d87e1
fix(java): strip optional/nullable wrappers from snippet list items (…
patrickthornton Apr 29, 2026
151e9cd
chore(typescript): release 3.66.5
github-actions[bot] Apr 29, 2026
ad435ab
chore(python): update python-sdk seed (#15571)
fern-support Apr 29, 2026
0428a39
chore(php): update php-sdk seed (#15570)
fern-support Apr 29, 2026
16332cf
chore(typescript): update ts-sdk seed (#15574)
fern-support Apr 29, 2026
00ed79f
chore(java): update java-sdk seed (#15576)
fern-support Apr 29, 2026
80b1bd4
chore(openapi): update openapi seed (#15572)
fern-support Apr 29, 2026
a5ffd37
chore(csharp): update csharp-sdk seed (#15578)
fern-support Apr 29, 2026
627fbf8
chore(swift): update swift-sdk seed (#15573)
fern-support Apr 29, 2026
e02d3ce
chore(go): update go-sdk seed (#15575)
fern-support Apr 29, 2026
d2585ae
chore(rust): update rust-sdk seed (#15579)
fern-support Apr 29, 2026
1a63536
chore(ruby): update ruby-sdk-v2 seed (#15577)
fern-support Apr 29, 2026
a13d60a
chore(java): update java-sdk seed (#15580)
fern-support Apr 29, 2026
09372f0
chore(typescript): update ts-sdk seed (#15581)
fern-support Apr 29, 2026
73460d2
chore(php): update php-sdk seed (#15582)
fern-support Apr 29, 2026
6f272d0
fix(ruby): strip trailing whitespace from generated comments (#15538)
devin-ai-integration[bot] Apr 29, 2026
1b98ece
chore(java): release 4.6.3
github-actions[bot] Apr 29, 2026
a3d9e9b
chore(php): release 2.8.0
github-actions[bot] Apr 29, 2026
0230cd2
chore(ruby-v2): release 1.11.1
github-actions[bot] Apr 29, 2026
f1b1d99
fix(csharp): percent-encode query param keys in WireMock test generat…
fern-support Apr 29, 2026
59418e3
chore(python): update python-sdk seed (#15584)
fern-support Apr 29, 2026
068e3b8
chore(java): update java-sdk seed (#15586)
fern-support Apr 29, 2026
49348e2
chore(php): update php-sdk seed (#15585)
fern-support Apr 29, 2026
f41de81
chore(csharp): release 2.63.1
github-actions[bot] Apr 29, 2026
3574abe
fix(csharp): avoid list-of-list wrap for already-list query param exa…
patrickthornton Apr 29, 2026
41251fe
chore(csharp): release 2.63.2
github-actions[bot] Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- summary: |
Fix WireMock test generation to use percent-encoded query parameter keys,
matching what spec-compliant HTTP clients send. Also split comma-delimited
query parameter examples into multiple values in a single `WithParam()` call,
since WireMock.Net parses comma-separated values into arrays.
type: fix
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- summary: |
Fix multi-value query parameter snippets when the example value is already
a list, which previously produced uncompilable list-of-list initializers
like `[new List<T>() { value }]` in mock server tests, reference docs, and
snippets.
type: fix
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,13 @@ export class MockEndpointGenerator extends WithGeneration {
for (const parameter of example.queryParameters) {
const maybeParameterValue = this.exampleToQueryOrHeaderValue(parameter);
if (maybeParameterValue != null) {
writer.write(`.WithParam("${getWireValue(parameter.name)}", "${maybeParameterValue}")`);
const encodedKey = percentEncodeQueryKey(getWireValue(parameter.name));
// WireMock.Net splits comma-delimited query values into separate array
// entries, so pass all values in a single WithParam call.
const paramValues = maybeParameterValue
.split(",")
.map((v) => `"${v.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`);
writer.write(`.WithParam("${encodedKey}", ${paramValues.join(", ")})`);
}
}
for (const header of [...example.serviceHeaders, ...example.endpointHeaders]) {
Expand Down Expand Up @@ -973,3 +979,27 @@ export class MockEndpointGenerator extends WithGeneration {
return pairs.join(", ");
}
}

// Characters the C# SDK's QueryStringBuilder treats as safe for query keys.
// Mirrors: unreserved + (sub-delims \ {& = +}) + : @ / ?
// See QueryStringBuilder.Template.cs SafeQueryKeyChars.
const SAFE_QUERY_KEY_CHARS = new Set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~!$'()*,;:@/?");

/**
* Percent-encodes a query parameter key to match the C# SDK's QueryStringBuilder.
* Characters not in SafeQueryKeyChars are percent-encoded with uppercase hex digits.
*/
function percentEncodeQueryKey(key: string): string {
const encoder = new TextEncoder();
let encoded = "";
for (const char of key) {
if (SAFE_QUERY_KEY_CHARS.has(char)) {
encoded += char;
} else {
for (const byte of encoder.encode(char)) {
encoded += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
}
}
}
return encoded;
}
Original file line number Diff line number Diff line change
Expand Up @@ -331,19 +331,22 @@ export class WrappedRequestGenerator extends FileGenerator<CSharpFile, SdkGenera
for (const exampleQueryParameter of example.queryParameters) {
const isSingleQueryParameter =
exampleQueryParameter.shape == null || exampleQueryParameter.shape.type === "single";
const exampleShape = exampleQueryParameter.value.shape;
const isExampleAlreadyList = exampleShape.type === "container" && exampleShape.container.type === "list";
const singleValueSnippet = this.exampleGenerator.getSnippetForTypeReference({
exampleTypeReference: exampleQueryParameter.value,
parseDatetimes
});
const value = isSingleQueryParameter
? singleValueSnippet
: this.csharp.codeblock((writer: Writer) =>
writer.writeNode(
this.csharp.list({
entries: [singleValueSnippet]
})
)
);
const value =
isSingleQueryParameter || isExampleAlreadyList
? singleValueSnippet
: this.csharp.codeblock((writer: Writer) =>
writer.writeNode(
this.csharp.list({
entries: [singleValueSnippet]
})
)
);
orderedFields.push({
name: exampleQueryParameter.name,
value
Expand Down
20 changes: 20 additions & 0 deletions generators/csharp/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 2.63.2
changelogEntry:
- summary: |
Fix multi-value query parameter snippets when the example value is already
a list, which previously produced uncompilable list-of-list initializers
like `[new List<T>() { value }]` in mock server tests, reference docs, and
snippets.
type: fix
createdAt: "2026-04-29"
irVersion: 66
- version: 2.63.1
changelogEntry:
- summary: |
Fix WireMock test generation to use percent-encoded query parameter keys,
matching what spec-compliant HTTP clients send. Also split comma-delimited
query parameter examples into multiple values in a single `WithParam()` call,
since WireMock.Net parses comma-separated values into arrays.
type: fix
createdAt: "2026-04-29"
irVersion: 66
- version: 2.63.0
changelogEntry:
- summary: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export const baseGoCustomConfigSchema = z.strictObject({
customPagerName: z.string().optional(),
offsetSemantics: z.enum(["item-index", "page-index"]).optional(),
omitFernHeaders: z.boolean().optional(),
maxRetries: z.number().int().min(0).optional()
maxRetries: z.number().int().min(0).optional(),
retryStatusCodes: z.optional(z.enum(["legacy", "recommended"]))
});

export type BaseGoCustomConfigSchema = z.infer<typeof baseGoCustomConfigSchema>;
4 changes: 1 addition & 3 deletions generators/go-v2/base/src/asIs/internal/retrier.go_
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,7 @@ func (r *Retrier) run(
// shouldRetry returns true if the request should be retried based on the given
// response status code.
func (r *Retrier) shouldRetry(response *http.Response) bool {
return response.StatusCode == http.StatusTooManyRequests ||
response.StatusCode == http.StatusRequestTimeout ||
response.StatusCode >= http.StatusInternalServerError
return {{RETRY_STATUS_CHECK}}
}

// retryDelay calculates the delay time based on response headers,
Expand Down
6 changes: 6 additions & 0 deletions generators/go-v2/base/src/project/GoProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ export class GoProject extends AbstractProject<AbstractGoGeneratorContext<BaseGo
contents = contents.replace(doubleRegex, value);
}

const retryStatusCheck =
this.context.customConfig.retryStatusCodes === "recommended"
? "response.StatusCode == http.StatusTooManyRequests ||\n\t\tresponse.StatusCode == http.StatusRequestTimeout ||\n\t\tresponse.StatusCode == http.StatusBadGateway ||\n\t\tresponse.StatusCode == http.StatusServiceUnavailable ||\n\t\tresponse.StatusCode == http.StatusGatewayTimeout"
: "response.StatusCode == http.StatusTooManyRequests ||\n\t\tresponse.StatusCode == http.StatusRequestTimeout ||\n\t\tresponse.StatusCode >= http.StatusInternalServerError";
contents = contents.replace(/\{\{RETRY_STATUS_CHECK\}\}/g, retryStatusCheck);

return new File(filename.replace(".go_", ".go"), RelativeFilePath.of(""), contents);
}

Expand Down
12 changes: 10 additions & 2 deletions generators/go-v2/sdk/features.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,19 @@ features:
as the request is deemed retryable and the number of retry attempts has not grown larger than the configured
retry limit (default: 2).

A request is deemed retryable when any of the following HTTP status codes is returned:
Which status codes are retried depends on the `retryStatusCodes` generator configuration:

**`legacy`** (current default): retries on
- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout)
- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests)
- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors)
- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses) (All server errors, including 500)

**`recommended`**: retries on
- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout)
- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests)
- [502](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) (Bad Gateway)
- [503](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) (Service Unavailable)
- [504](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) (Gateway Timeout)

If the `Retry-After` header is present in the response, the SDK will prioritize respecting its value exactly
over the default exponential backoff.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (r *Retrier) run(
func (r *Retrier) shouldRetry(response *http.Response) bool {
return response.StatusCode == http.StatusTooManyRequests ||
response.StatusCode == http.StatusRequestTimeout ||
response.StatusCode >= http.StatusInternalServerError
(response.StatusCode > http.StatusInternalServerError && response.StatusCode < 600)
}

// retryDelay calculates the delay time in milliseconds based on the retry attempt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,24 @@ func TestRetrier(t *testing.T) {
giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK},
},
{
description: "retries occur on status code 500",
description: "retries occur on status code 502",
giveAttempts: 2,
giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK},
giveStatusCodes: []int{http.StatusBadGateway, http.StatusOK},
},
{
description: "no retries on status code 500",
giveAttempts: 1,
giveStatusCodes: []int{http.StatusInternalServerError},
},
{
description: "retries occur on status code 501",
giveAttempts: 2,
giveStatusCodes: []int{501, http.StatusOK},
},
{
description: "retries occur on status code 599",
giveAttempts: 2,
giveStatusCodes: []int{599, http.StatusOK},
},
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (r *Retrier) run(
func (r *Retrier) shouldRetry(response *http.Response) bool {
return response.StatusCode == http.StatusTooManyRequests ||
response.StatusCode == http.StatusRequestTimeout ||
response.StatusCode >= http.StatusInternalServerError
(response.StatusCode > http.StatusInternalServerError && response.StatusCode < 600)
}

// retryDelay calculates the delay time in milliseconds based on the retry attempt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,24 @@ func TestRetrier(t *testing.T) {
giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK},
},
{
description: "retries occur on status code 500",
description: "retries occur on status code 502",
giveAttempts: 2,
giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK},
giveStatusCodes: []int{http.StatusBadGateway, http.StatusOK},
},
{
description: "no retries on status code 500",
giveAttempts: 1,
giveStatusCodes: []int{http.StatusInternalServerError},
},
{
description: "retries occur on status code 501",
giveAttempts: 2,
giveStatusCodes: []int{501, http.StatusOK},
},
{
description: "retries occur on status code 599",
giveAttempts: 2,
giveStatusCodes: []int{599, http.StatusOK},
},
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (r *Retrier) run(
func (r *Retrier) shouldRetry(response *http.Response) bool {
return response.StatusCode == http.StatusTooManyRequests ||
response.StatusCode == http.StatusRequestTimeout ||
response.StatusCode >= http.StatusInternalServerError
(response.StatusCode > http.StatusInternalServerError && response.StatusCode < 600)
}

// retryDelay calculates the delay time in milliseconds based on the retry attempt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,24 @@ func TestRetrier(t *testing.T) {
giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK},
},
{
description: "retries occur on status code 500",
description: "retries occur on status code 502",
giveAttempts: 2,
giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK},
giveStatusCodes: []int{http.StatusBadGateway, http.StatusOK},
},
{
description: "no retries on status code 500",
giveAttempts: 1,
giveStatusCodes: []int{http.StatusInternalServerError},
},
{
description: "retries occur on status code 501",
giveAttempts: 2,
giveStatusCodes: []int{501, http.StatusOK},
},
{
description: "retries occur on status code 599",
giveAttempts: 2,
giveStatusCodes: []int{599, http.StatusOK},
},
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (r *Retrier) run(
func (r *Retrier) shouldRetry(response *http.Response) bool {
return response.StatusCode == http.StatusTooManyRequests ||
response.StatusCode == http.StatusRequestTimeout ||
response.StatusCode >= http.StatusInternalServerError
(response.StatusCode > http.StatusInternalServerError && response.StatusCode < 600)
}

// retryDelay calculates the delay time in milliseconds based on the retry attempt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,24 @@ func TestRetrier(t *testing.T) {
giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK},
},
{
description: "retries occur on status code 500",
description: "retries occur on status code 502",
giveAttempts: 2,
giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK},
giveStatusCodes: []int{http.StatusBadGateway, http.StatusOK},
},
{
description: "no retries on status code 500",
giveAttempts: 1,
giveStatusCodes: []int{http.StatusInternalServerError},
},
{
description: "retries occur on status code 501",
giveAttempts: 2,
giveStatusCodes: []int{501, http.StatusOK},
},
{
description: "retries occur on status code 599",
giveAttempts: 2,
giveStatusCodes: []int{599, http.StatusOK},
},
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (r *Retrier) run(
func (r *Retrier) shouldRetry(response *http.Response) bool {
return response.StatusCode == http.StatusTooManyRequests ||
response.StatusCode == http.StatusRequestTimeout ||
response.StatusCode >= http.StatusInternalServerError
(response.StatusCode > http.StatusInternalServerError && response.StatusCode < 600)
}

// retryDelay calculates the delay time in milliseconds based on the retry attempt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,24 @@ func TestRetrier(t *testing.T) {
giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK},
},
{
description: "retries occur on status code 500",
description: "retries occur on status code 502",
giveAttempts: 2,
giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK},
giveStatusCodes: []int{http.StatusBadGateway, http.StatusOK},
},
{
description: "no retries on status code 500",
giveAttempts: 1,
giveStatusCodes: []int{http.StatusInternalServerError},
},
{
description: "retries occur on status code 501",
giveAttempts: 2,
giveStatusCodes: []int{501, http.StatusOK},
},
{
description: "retries occur on status code 599",
giveAttempts: 2,
giveStatusCodes: []int{599, http.StatusOK},
},
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (r *Retrier) run(
func (r *Retrier) shouldRetry(response *http.Response) bool {
return response.StatusCode == http.StatusTooManyRequests ||
response.StatusCode == http.StatusRequestTimeout ||
response.StatusCode >= http.StatusInternalServerError
(response.StatusCode > http.StatusInternalServerError && response.StatusCode < 600)
}

// retryDelay calculates the delay time in milliseconds based on the retry attempt.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,24 @@ func TestRetrier(t *testing.T) {
giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK},
},
{
description: "retries occur on status code 500",
description: "retries occur on status code 502",
giveAttempts: 2,
giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK},
giveStatusCodes: []int{http.StatusBadGateway, http.StatusOK},
},
{
description: "no retries on status code 500",
giveAttempts: 1,
giveStatusCodes: []int{http.StatusInternalServerError},
},
{
description: "retries occur on status code 501",
giveAttempts: 2,
giveStatusCodes: []int{501, http.StatusOK},
},
{
description: "retries occur on status code 599",
giveAttempts: 2,
giveStatusCodes: []int{599, http.StatusOK},
},
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (r *Retrier) run(
func (r *Retrier) shouldRetry(response *http.Response) bool {
return response.StatusCode == http.StatusTooManyRequests ||
response.StatusCode == http.StatusRequestTimeout ||
response.StatusCode >= http.StatusInternalServerError
(response.StatusCode > http.StatusInternalServerError && response.StatusCode < 600)
}

// retryDelay calculates the delay time in milliseconds based on the retry attempt.
Expand Down
Loading
Loading