Skip to content
Open
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
24 changes: 24 additions & 0 deletions internal/test/server-specific/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Server-specific tests
paths:
# The dashed path parameter name "addressing-identifier" is not a valid Go
# identifier. This exercises GitHub issue #2278: stdhttp's ServeMux requires
# wildcard names to be valid Go identifiers.
/resources/{addressing-identifier}:
get:
operationId: getResource
parameters:
- name: addressing-identifier
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
text/plain:
schema:
type: string
6 changes: 6 additions & 0 deletions internal/test/server-specific/stdhttp/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# yaml-language-server: $schema=../../../../configuration-schema.json
package: stdhttp
generate:
std-http-server: true
models: true
output: server.gen.go
3 changes: 3 additions & 0 deletions internal/test/server-specific/stdhttp/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package stdhttp

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=config.yaml ../spec.yaml
179 changes: 179 additions & 0 deletions internal/test/server-specific/stdhttp/server.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions internal/test/server-specific/stdhttp/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//go:build go1.22

package stdhttp

import (
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

type testServer struct {
receivedParam string
}

func (s *testServer) GetResource(w http.ResponseWriter, r *http.Request, addressingIdentifier string) {
s.receivedParam = addressingIdentifier
_, _ = fmt.Fprint(w, addressingIdentifier)
}

func TestDashedPathParam(t *testing.T) {
server := &testServer{}
handler := Handler(server)

req := httptest.NewRequest(http.MethodGet, "/resources/my-value", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code, "expected 200 OK, got %d; body: %s", rec.Code, rec.Body.String())
assert.Equal(t, "my-value", server.receivedParam, "path parameter was not correctly extracted")
}
8 changes: 8 additions & 0 deletions pkg/codegen/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ func (pd *ParameterDefinition) SchemaFormat() string {
return ""
}

// SanitizedParamName returns the parameter name sanitized to be a valid Go
// identifier. This is needed for routers like net/http's ServeMux where path
// wildcards (e.g. {name}) must be valid Go identifiers. For the original
// OpenAPI parameter name (e.g. for error messages or JSON tags), use ParamName.
func (pd ParameterDefinition) SanitizedParamName() string {
return SanitizeGoIdentifier(pd.ParamName)
}

func (pd ParameterDefinition) GoVariableName() string {
name := LowercaseFirstCharacters(pd.GoName())
if IsGoKeyword(name) {
Expand Down
6 changes: 3 additions & 3 deletions pkg/codegen/templates/stdhttp/std-http-middleware.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ func (siw *ServerInterfaceWrapper) {{$opid}}(w http.ResponseWriter, r *http.Requ
var {{$varName := .GoVariableName}}{{$varName}} {{.TypeDef}}

{{if .IsPassThrough}}
{{$varName}} = r.PathValue("{{.ParamName}}")
{{$varName}} = r.PathValue("{{.SanitizedParamName}}")
{{end}}
{{if .IsJson}}
err = json.Unmarshal([]byte(r.PathValue("{{.ParamName}}")), &{{$varName}})
err = json.Unmarshal([]byte(r.PathValue("{{.SanitizedParamName}}")), &{{$varName}})
if err != nil {
siw.ErrorHandlerFunc(w, r, &UnmarshalingParamError{ParamName: "{{.ParamName}}", Err: err})
return
}
{{end}}
{{if .IsStyled}}
err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", r.PathValue("{{.ParamName}}"), &{{$varName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"})
err = runtime.BindStyledParameterWithOptions("{{.Style}}", "{{.ParamName}}", r.PathValue("{{.SanitizedParamName}}"), &{{$varName}}, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: {{.Explode}}, Required: {{.Required}}, Type: "{{.SchemaType}}", Format: "{{.SchemaFormat}}"})
if err != nil {
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "{{.ParamName}}", Err: err})
return
Expand Down
28 changes: 21 additions & 7 deletions pkg/codegen/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -638,8 +638,9 @@ func SwaggerUriToGorillaUri(uri string) string {
}

// SwaggerUriToStdHttpUri converts a swagger style path URI with parameters to a
// Chi compatible path URI. We need to replace all Swagger parameters with
// "{param}". Valid input parameters are:
// net/http ServeMux compatible path URI. Parameter names are sanitized to be
// valid Go identifiers, as required by ServeMux wildcard segments. Valid input
// parameters are:
//
// {param}
// {param*}
Expand All @@ -656,7 +657,10 @@ func SwaggerUriToStdHttpUri(uri string) string {
return "/{$}"
}

return pathParamRE.ReplaceAllString(uri, "{$1}")
return pathParamRE.ReplaceAllStringFunc(uri, func(match string) string {
sub := pathParamRE.FindStringSubmatch(match)
return "{" + SanitizeGoIdentifier(sub[1]) + "}"
})
Comment on lines +660 to +663
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sanitization collision: two distinct parameter names can map to the same wildcard

SanitizeGoIdentifier replaces every illegal rune with _, so two different OpenAPI parameter names can produce the same sanitized output. For example, my-param and my_param both produce my_param.

If a spec defines both in the same path (or if two separate parameters accidentally collide), the route registered with ServeMux would contain a duplicate wildcard segment, e.g.:

GET /resources/{my_param}/{my_param}

Go's net/http ServeMux panics when it encounters a pattern with two wildcards that have the same name (see Go source). This would cause a runtime panic at server startup rather than a compile-time error.

Consider detecting this collision during code generation (e.g. in SwaggerUriToStdHttpUri or the calling site) and returning an error, or at minimum documenting the limitation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I am ok with the possibility to rename something into a new conflict in this very limited case. This is at such a lower level than our name conflict resolution code, that combining the two would be incredibly messy.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we feel that given #2220 / #1574 we'd not get into this case? Should we at least add a test to capture "we don't support this"?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, PR #2220 handles a different scenario. It addresses intentional duplicates in the spec (like /clients/{client-uuid}/roles/{role-name}/composites/clients/{client-uuid} where client-uuid genuinely appears twice), deduplicating them at the ParameterDefinition struct level.

The collision risk here is about accidental duplicates created by sanitization: two originally distinct parameter names (e.g., {my-param} and {my_param}) both becoming {my_param} in the route pattern string that ServeMux sees, causing a panic when registering the route.

Since #2220's deduplication operates on parameter definitions (before URI transformation), it wouldn't prevent ServeMux from seeing duplicate wildcards in the pattern string after sanitization.

Adding a test that documents "we don't support specs where sanitization creates collisions" would be valuable - it makes the limitation explicit and prevents confusion if someone hits this edge case. Something like:

// Test that specs with parameters that collide after sanitization are not supported
// (e.g., {my-param} and {my_param} both become {my_param})

This also helps future maintainers understand the behavior is intentional, not an overlooked bug.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#2220 is a different case - it's when path parameters are used more than once in a path. Given the order of evaluation, that fix doesn't help here.

}

// OrderedParamsFromUri returns the argument names, in order, in a given URI string, so for
Expand Down Expand Up @@ -755,9 +759,12 @@ func IsValidGoIdentity(str string) bool {
return !IsPredeclaredGoIdentifier(str)
}

// SanitizeGoIdentity deletes and replaces the illegal runes in the given
// string to use the string as a valid identity.
func SanitizeGoIdentity(str string) string {
// SanitizeGoIdentifier replaces illegal runes in the given string so that
// it is a valid Go identifier. Unlike SanitizeGoIdentity, it does not
// prefix reserved keywords or predeclared identifiers. This is useful for
// contexts where the name must be a valid identifier but is not used as a
// Go symbol (e.g. net/http ServeMux wildcard names).
func SanitizeGoIdentifier(str string) string {
sanitized := []rune(str)

for i, c := range sanitized {
Expand All @@ -768,7 +775,14 @@ func SanitizeGoIdentity(str string) string {
}
}

str = string(sanitized)
return string(sanitized)
}

// SanitizeGoIdentity deletes and replaces the illegal runes in the given
// string to use the string as a valid identity. It also prefixes reserved
// keywords and predeclared identifiers with an underscore.
func SanitizeGoIdentity(str string) string {
str = SanitizeGoIdentifier(str)

if IsGoKeyword(str) || IsPredeclaredGoIdentifier(str) {
str = "_" + str
Expand Down
7 changes: 7 additions & 0 deletions pkg/codegen/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,13 @@ func TestSwaggerUriToStdHttpUriUri(t *testing.T) {
assert.Equal(t, "/path/{arg}/foo", SwaggerUriToStdHttpUri("/path/{;arg*}/foo"))
assert.Equal(t, "/path/{arg}/foo", SwaggerUriToStdHttpUri("/path/{?arg}/foo"))
assert.Equal(t, "/path/{arg}/foo", SwaggerUriToStdHttpUri("/path/{?arg*}/foo"))

// Parameter names that are not valid Go identifiers must be sanitized (issue #2278)
assert.Equal(t, "/path/{addressing_identifier}", SwaggerUriToStdHttpUri("/path/{addressing-identifier}"))
assert.Equal(t, "/path/{my_param}/{other_param}", SwaggerUriToStdHttpUri("/path/{my-param}/{other-param}"))

// Go keywords are valid ServeMux wildcard names and should not be prefixed
assert.Equal(t, "/path/{type}", SwaggerUriToStdHttpUri("/path/{type}"))
}

func TestOrderedParamsFromUri(t *testing.T) {
Expand Down