Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,34 @@ namespace {{packageName}}.Client
return obj != null ? Newtonsoft.Json.JsonConvert.SerializeObject(obj) : null;
}

/// <summary>
/// Serializes the given object as an ASCII-safe JSON string. Used to serialize the value of
/// a header parameter whose content type is JSON (e.g. <c>content: application/json</c>),
/// so model objects are rendered as JSON instead of their debug ToString() representation.
/// </summary>
/// <param name="obj">The object to serialize.</param>
/// <returns>JSON representation of the object with every C0 control character (including CR and LF), DEL, and non-ASCII character escaped as \uXXXX, so raw control bytes can never reach the header value.</returns>
public static string ParameterToJsonString(object obj)
{
// JsonConvert renders a null reference as the JSON literal "null" (not a null string).
string json = Newtonsoft.Json.JsonConvert.SerializeObject(obj);

// Escape everything that is not printable ASCII: C0 controls (including CR and LF), DEL,
// and all non-ASCII characters, as \uXXXX. Because this runs over the final serialized
// string, the result is safe to use as an HTTP header value even if a custom converter
// emitted raw control characters or non-ASCII text (preventing header splitting/injection).
StringBuilder escaped = new StringBuilder(json.Length);
foreach (char c in json)
{
if (c < 0x20 || c >= 0x7f)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
escaped.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
else
escaped.Append(c);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}

return escaped.ToString();
}

/// <summary>
/// Encode string in base64 format.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,12 +349,12 @@ namespace {{packageName}}.{{apiPackage}}
{{/queryParams}}
{{#headerParams}}
{{#required}}
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
{{/required}}
{{^required}}
if ({{paramName}} != null)
{
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
}
{{/required}}
{{/headerParams}}
Expand Down Expand Up @@ -639,12 +639,12 @@ namespace {{packageName}}.{{apiPackage}}
{{/constantParams}}
{{#headerParams}}
{{#required}}
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
{{/required}}
{{^required}}
if ({{paramName}} != null)
{
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
}
{{/required}}
{{/headerParams}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -578,11 +578,11 @@ namespace {{packageName}}.{{apiPackage}}
// Set client side default value of Header Param "{{baseName}}".
if (ClientUtils.IsContentHeader("{{baseName}}"))
{
httpRequestMessageLocalVar.Content?.Headers.Add("{{baseName}}", ClientUtils.ParameterToString({{paramName}}));
httpRequestMessageLocalVar.Content?.Headers.Add("{{baseName}}", {{#headerIsJsonMimeType}}JsonSerializer.Serialize({{paramName}}, _jsonSerializerOptions){{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ClientUtils.ParameterToString({{paramName}}){{/headerIsJsonMimeType}});
}
else
{
httpRequestMessageLocalVar.Headers.Add("{{baseName}}", ClientUtils.ParameterToString({{paramName}}));
httpRequestMessageLocalVar.Headers.Add("{{baseName}}", {{#headerIsJsonMimeType}}JsonSerializer.Serialize({{paramName}}, _jsonSerializerOptions){{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ClientUtils.ParameterToString({{paramName}}){{/headerIsJsonMimeType}});
}

{{/required}}
Expand All @@ -592,11 +592,11 @@ namespace {{packageName}}.{{apiPackage}}
// Set client side default value of Header Param "{{baseName}}".
if (ClientUtils.IsContentHeader("{{baseName}}"))
{
httpRequestMessageLocalVar.Content?.Headers.Add("{{baseName}}", ClientUtils.ParameterToString({{paramName}}.Value));
httpRequestMessageLocalVar.Content?.Headers.Add("{{baseName}}", {{#headerIsJsonMimeType}}JsonSerializer.Serialize({{paramName}}.Value, _jsonSerializerOptions){{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ClientUtils.ParameterToString({{paramName}}.Value){{/headerIsJsonMimeType}});
}
else
{
httpRequestMessageLocalVar.Headers.Add("{{baseName}}", ClientUtils.ParameterToString({{paramName}}.Value));
httpRequestMessageLocalVar.Headers.Add("{{baseName}}", {{#headerIsJsonMimeType}}JsonSerializer.Serialize({{paramName}}.Value, _jsonSerializerOptions){{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ClientUtils.ParameterToString({{paramName}}.Value){{/headerIsJsonMimeType}});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,12 +430,12 @@ namespace {{packageName}}.{{apiPackage}}
{{/queryParams}}
{{#headerParams}}
{{#required}}
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
{{/required}}
{{^required}}
if ({{paramName}} != null)
{
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
}
{{/required}}
{{/headerParams}}
Expand Down Expand Up @@ -647,12 +647,12 @@ namespace {{packageName}}.{{apiPackage}}
{{/constantParams}}
{{#headerParams}}
{{#required}}
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
{{/required}}
{{^required}}
if ({{paramName}} != null)
{
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
}
{{/required}}
{{/headerParams}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,12 +356,12 @@ namespace {{packageName}}.{{apiPackage}}
{{/queryParams}}
{{#headerParams}}
{{#required}}
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
{{/required}}
{{^required}}
if ({{paramName}} != null)
{
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
}
{{/required}}
{{/headerParams}}
Expand Down Expand Up @@ -567,12 +567,12 @@ namespace {{packageName}}.{{apiPackage}}
{{/queryParams}}
{{#headerParams}}
{{#required}}
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
{{/required}}
{{^required}}
if ({{paramName}} != null)
{
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
}
{{/required}}
{{/headerParams}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,69 @@ public void testHandleConstantParams() throws IOException {
"localVarRequestOptions.HeaderParameters.Add(\"X-CUSTOM_CONSTANT_HEADER\", Org.OpenAPITools.Client.ClientUtils.ParameterToString(\"CONSTANT_VALUE\"));");
}

@Test
public void testJsonContentHeaderUsesJsonSerialization() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_1/csharp/json-header-content.yaml");
final DefaultGenerator defaultGenerator = new DefaultGenerator();
final ClientOptInput clientOptInput = new ClientOptInput();
clientOptInput.openAPI(openAPI);
CSharpClientCodegen cSharpClientCodegen = new CSharpClientCodegen();
cSharpClientCodegen.setLibrary("restsharp");
cSharpClientCodegen.setOutputDir(output.getAbsolutePath());
clientOptInput.config(cSharpClientCodegen);
defaultGenerator.opts(clientOptInput);

Map<String, File> files = defaultGenerator.generate().stream()
.collect(Collectors.toMap(File::getPath, Function.identity()));

File apiFile = files
.get(Paths.get(output.getAbsolutePath(), "src", "Org.OpenAPITools", "Api", "DefaultApi.cs").toString());
assertNotNull(apiFile);
// JSON-content header is serialized as JSON, not via ParameterToString (which would emit the model's debug ToString()).
assertFileContains(apiFile.toPath(),
"localVarRequestOptions.HeaderParameters.Add(\"X-Json-Arg\", Org.OpenAPITools.Client.ClientUtils.ParameterToJsonString(xJsonArg)); // header parameter");
assertFileNotContains(apiFile.toPath(),
"localVarRequestOptions.HeaderParameters.Add(\"X-Json-Arg\", Org.OpenAPITools.Client.ClientUtils.ParameterToString(xJsonArg)); // header parameter");
// A vendor JSON media type (application/vnd.*+json) is also serialized as JSON.
assertFileContains(apiFile.toPath(),
"localVarRequestOptions.HeaderParameters.Add(\"X-Vendor-Json-Arg\", Org.OpenAPITools.Client.ClientUtils.ParameterToJsonString(xVendorJsonArg)); // header parameter");
// A regular (non-JSON) header keeps the existing behavior.
assertFileContains(apiFile.toPath(),
"localVarRequestOptions.HeaderParameters.Add(\"X-Plain-Arg\", Org.OpenAPITools.Client.ClientUtils.ParameterToString(xPlainArg)); // header parameter");
}

@Test
public void testJsonContentHeaderUsesJsonSerializationGenericHost() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_1/csharp/json-header-content.yaml");
final DefaultGenerator defaultGenerator = new DefaultGenerator();
final ClientOptInput clientOptInput = new ClientOptInput();
clientOptInput.openAPI(openAPI);
CSharpClientCodegen cSharpClientCodegen = new CSharpClientCodegen();
cSharpClientCodegen.setLibrary("generichost");
cSharpClientCodegen.setOutputDir(output.getAbsolutePath());
clientOptInput.config(cSharpClientCodegen);
defaultGenerator.opts(clientOptInput);

Map<String, File> files = defaultGenerator.generate().stream()
.collect(Collectors.toMap(File::getPath, Function.identity()));

File apiFile = files
.get(Paths.get(output.getAbsolutePath(), "src", "Org.OpenAPITools", "Api", "DefaultApi.cs").toString());
assertNotNull(apiFile);
// JSON-content header is serialized with System.Text.Json using the client's serializer options.
assertFileContains(apiFile.toPath(),
"JsonSerializer.Serialize(xJsonArg, _jsonSerializerOptions)");
// A vendor JSON media type (application/vnd.*+json) is also serialized as JSON.
assertFileContains(apiFile.toPath(),
"JsonSerializer.Serialize(xVendorJsonArg, _jsonSerializerOptions)");
assertFileContains(apiFile.toPath(),
"ClientUtils.ParameterToString(xPlainArg)");
}

@Test
public void testUserAgentIsNotUrlEncoded() throws IOException {
// both restsharp Configuration templates: the default one and the useIntForTimeout v7.9.0 fallback
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
openapi: 3.1.0

info:
title: JSON header repro
version: 1.0.0

paths:
/test:
post:
operationId: testJsonHeader
parameters:
- name: X-Json-Arg
in: header
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/HeaderArg'
- name: X-Vendor-Json-Arg
in: header
required: true
content:
application/vnd.acme.v1+json:
schema:
$ref: '#/components/schemas/HeaderArg'
- name: X-Plain-Arg
in: header
required: true
schema:
type: string
responses:
'204':
description: OK

components:
schemas:
HeaderArg:
type: object
properties:
path:
type: string
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,34 @@ public static string Serialize(object obj)
return obj != null ? Newtonsoft.Json.JsonConvert.SerializeObject(obj) : null;
}

/// <summary>
/// Serializes the given object as an ASCII-safe JSON string. Used to serialize the value of
/// a header parameter whose content type is JSON (e.g. <c>content: application/json</c>),
/// so model objects are rendered as JSON instead of their debug ToString() representation.
/// </summary>
/// <param name="obj">The object to serialize.</param>
/// <returns>JSON representation of the object with every C0 control character (including CR and LF), DEL, and non-ASCII character escaped as \uXXXX, so raw control bytes can never reach the header value.</returns>
public static string ParameterToJsonString(object obj)
{
// JsonConvert renders a null reference as the JSON literal "null" (not a null string).
string json = Newtonsoft.Json.JsonConvert.SerializeObject(obj);

// Escape everything that is not printable ASCII: C0 controls (including CR and LF), DEL,
// and all non-ASCII characters, as \uXXXX. Because this runs over the final serialized
// string, the result is safe to use as an HTTP header value even if a custom converter
// emitted raw control characters or non-ASCII text (preventing header splitting/injection).
StringBuilder escaped = new StringBuilder(json.Length);
foreach (char c in json)
{
if (c < 0x20 || c >= 0x7f)
escaped.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
else
escaped.Append(c);
}

return escaped.ToString();
}

/// <summary>
/// Encode string in base64 format.
/// </summary>
Expand Down
Loading
Loading