From f68595576940ed3523ccf4263f90eceac3ff4c75 Mon Sep 17 00:00:00 2001 From: Solaris-star <820622658@qq.com> Date: Wed, 22 Jul 2026 02:05:40 +0800 Subject: [PATCH] fix(stdhttp): reject mixed path-parameter segments at codegen net/http ServeMux requires wildcards to occupy an entire path segment. Paths like /resources/{resourceId}:apply previously generated patterns that panic at server registration. Validate std-http-server paths at codegen time and fail with a clear error instead of emitting unusable code. Fixes #2488 --- pkg/codegen/codegen.go | 5 ++++ pkg/codegen/utils.go | 37 ++++++++++++++++++++++++ pkg/codegen/utils_test.go | 59 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+) diff --git a/pkg/codegen/codegen.go b/pkg/codegen/codegen.go index 90ced55a3..2f9bdeb69 100644 --- a/pkg/codegen/codegen.go +++ b/pkg/codegen/codegen.go @@ -178,6 +178,11 @@ func Generate(spec *openapi3.T, opts Configuration) (string, error) { if err := ValidateSpec(spec); err != nil { return "", err } + if opts.Generate.StdHTTPServer { + if err := ValidateStdHTTPPaths(spec); err != nil { + return "", err + } + } // if we are provided an override for the response type suffix update it if opts.OutputOptions.ResponseTypeSuffix != "" { diff --git a/pkg/codegen/utils.go b/pkg/codegen/utils.go index 55810bbb9..186885f94 100644 --- a/pkg/codegen/utils.go +++ b/pkg/codegen/utils.go @@ -15,6 +15,7 @@ package codegen import ( "bytes" + "errors" "cmp" "fmt" "go/token" @@ -682,6 +683,42 @@ func SwaggerUriToStdHttpUri(uri string) string { return uri } +// ValidateStdHTTPPath reports whether an OpenAPI path can be registered with +// net/http ServeMux. ServeMux wildcards must occupy an entire path segment, so +// mixed segments such as "{resourceId}:apply" are rejected at codegen time +// instead of panicking at server startup (see #2488). +func ValidateStdHTTPPath(path string) error { + for _, seg := range strings.Split(path, "/") { + if seg == "" || seg == "{$}" { + continue + } + loc := pathParamRE.FindStringIndex(seg) + if loc == nil { + // pure literal segment + continue + } + // Wildcard must be the entire segment. + if loc[0] != 0 || loc[1] != len(seg) { + return fmt.Errorf("path %q: segment %q mixes a path parameter with literal text; net/http ServeMux requires wildcards to occupy an entire path segment (std-http-server)", path, seg) + } + } + return nil +} + +// ValidateStdHTTPPaths validates every path in the document for ServeMux. +func ValidateStdHTTPPaths(spec *openapi3.T) error { + if spec == nil || spec.Paths == nil { + return nil + } + var errs []error + for _, path := range SortedMapKeys(spec.Paths.Map()) { + if err := ValidateStdHTTPPath(path); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + // OrderedParamsFromUri returns the argument names, in order, in a given URI string, so for // /path/{param1}/{.param2*}/{?param3}, it would return param1, param2, param3 func OrderedParamsFromUri(uri string) []string { diff --git a/pkg/codegen/utils_test.go b/pkg/codegen/utils_test.go index b1da98fe0..9f2df0b59 100644 --- a/pkg/codegen/utils_test.go +++ b/pkg/codegen/utils_test.go @@ -837,3 +837,62 @@ func Test_replaceInitialism(t *testing.T) { }) } } + + +func TestValidateStdHTTPPath(t *testing.T) { + tests := []struct { + path string + wantErr bool + }{ + {"/resources/{resourceId}", false}, + {"/resources/{resourceId}/apply", false}, + {"/resources/{resourceId}:apply", true}, + {"/resources/prefix{resourceId}", true}, + {"/resources/{resourceId}suffix", true}, + {"/pets:validate", false}, // pure literal segment with colon is fine for ServeMux + {"/path/{arg1}/{arg2}/foo", false}, + } + for _, tt := range tests { + err := ValidateStdHTTPPath(tt.path) + if tt.wantErr && err == nil { + t.Errorf("path %q: expected error", tt.path) + } + if !tt.wantErr && err != nil { + t.Errorf("path %q: unexpected error: %v", tt.path, err) + } + } +} + +func TestGenerateRejectsMixedServeMuxPathParam(t *testing.T) { + spec := `openapi: 3.0.3 +info: + title: mixed path param + version: 1.0.0 +paths: + /resources/{resourceId}:apply: + post: + operationId: applyResource + parameters: + - name: resourceId + in: path + required: true + schema: + type: string + responses: + "204": + description: Applied +` + loader := openapi3.NewLoader() + swagger, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + _, err = Generate(swagger, Configuration{ + PackageName: "api", + Generate: GenerateOptions{ + StdHTTPServer: true, + Models: true, + }, + }) + require.Error(t, err) + require.Contains(t, err.Error(), "mixes a path parameter") +}