From a6bf9713ac3c4c2f75245f91a39c31f639c44f4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 09:42:07 +0000 Subject: [PATCH 1/5] Initial plan From 782e95400ae26fd54d59f8cdb1963fb27dab26e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 09:45:11 +0000 Subject: [PATCH 2/5] Add test to demonstrate Result bug with both Code and Err Co-authored-by: cymoo <7800791+cymoo@users.noreply.github.com> --- mint_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/mint_test.go b/mint_test.go index 5b1977b..5ed175a 100644 --- a/mint_test.go +++ b/mint_test.go @@ -1966,3 +1966,35 @@ func TestCompleteConfigurationScenario(t *testing.T) { } }) } + +// ========== Bug Tests ========== + +func TestBugResultWithCodeAndErr(t *testing.T) { +Reset() + +t.Run("Result with both Code and Err - bug demonstration", func(t *testing.T) { +handler := H(func() Result[string] { +return Result[string]{ +Code: 403, // Forbidden +Err: errors.New("access denied"), +Data: "should not be returned", +} +}) + +rec := httptest.NewRecorder() +req := httptest.NewRequest("GET", "/", nil) +handler(rec, req) + +t.Logf("Status Code: %d", rec.Code) +t.Logf("Response Body: %s", rec.Body.String()) + +// The bug: status code will be 403 (from result.Code written first) +// but handleError tries to write 500 (from inferStatusCode) +// This causes inconsistency - status is 403 but error handling expects 500 + +// Current behavior: First WriteHeader(403) succeeds, second is ignored with warning +if rec.Code != 403 { +t.Errorf("expected status 403 (first WriteHeader wins), got %d", rec.Code) +} +}) +} From ba50c64dedc204635d01cd5c7bb0af017142fcc0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 09:48:27 +0000 Subject: [PATCH 3/5] Fix Result bug: handle Code and Err consistently Co-authored-by: cymoo <7800791+cymoo@users.noreply.github.com> --- mint.go | 21 +++++++++-- mint_test.go | 103 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 94 insertions(+), 30 deletions(-) diff --git a/mint.go b/mint.go index 43382e6..bfe7dcf 100644 --- a/mint.go +++ b/mint.go @@ -855,18 +855,26 @@ func handleResult(w http.ResponseWriter, result Result[any]) error { WriteHeaders(w, result.Headers) } - if result.Code != 0 { - w.WriteHeader(result.Code) - } - if result.Err != nil { + // If both Code and Err are set, use the explicit Code for the error response + if result.Code != 0 { + return handleErrorWithCode(w, result.Err, result.Code) + } return handleError(w, result.Err) } + if result.Code != 0 { + w.WriteHeader(result.Code) + } + return handleCommonTypes(w, result.Data) } func handleError(w http.ResponseWriter, err error) error { + return handleErrorWithCode(w, err, 0) +} + +func handleErrorWithCode(w http.ResponseWriter, err error, statusCode int) error { if errorHandler() != nil { errorHandler()(w, err) return nil @@ -882,6 +890,11 @@ func handleError(w http.ResponseWriter, err error) error { return nil } + // If an explicit status code is provided, use it instead of the inferred one + if statusCode != 0 { + httpErr.Code = statusCode + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") if !statusWritten { diff --git a/mint_test.go b/mint_test.go index 5ed175a..6236cd4 100644 --- a/mint_test.go +++ b/mint_test.go @@ -1967,34 +1967,85 @@ func TestCompleteConfigurationScenario(t *testing.T) { }) } -// ========== Bug Tests ========== - -func TestBugResultWithCodeAndErr(t *testing.T) { -Reset() - -t.Run("Result with both Code and Err - bug demonstration", func(t *testing.T) { -handler := H(func() Result[string] { -return Result[string]{ -Code: 403, // Forbidden -Err: errors.New("access denied"), -Data: "should not be returned", -} -}) +// ========== Bug Fix Tests ========== + +// TestResultWithCodeAndErr tests the fix for the bug where Result[T] with both +// Code and Err set would write status codes twice, causing inconsistency +func TestResultWithCodeAndErr(t *testing.T) { + Reset() + + t.Run("Result with both Code and Err - should use explicit Code", func(t *testing.T) { + handler := H(func() Result[string] { + return Result[string]{ + Code: 403, // Forbidden - should be used + Err: errors.New("access denied"), + Data: "should not be returned", + } + }) -rec := httptest.NewRecorder() -req := httptest.NewRequest("GET", "/", nil) -handler(rec, req) + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) -t.Logf("Status Code: %d", rec.Code) -t.Logf("Response Body: %s", rec.Body.String()) + t.Logf("Status Code: %d", rec.Code) + t.Logf("Response Body: %s", rec.Body.String()) + + // Fixed: Both HTTP status and error JSON code should be 403 + if rec.Code != 403 { + t.Errorf("expected HTTP status 403, got %d", rec.Code) + } + + var errResp HTTPError + parseJSONResponse(t, rec.Body.Bytes(), &errResp) + if errResp.Code != 403 { + t.Errorf("expected error code 403 in JSON, got %d", errResp.Code) + } + }) + + t.Run("Result with only Err - should infer code", func(t *testing.T) { + handler := H(func() Result[string] { + return Result[string]{ + Err: errors.New("something went wrong"), + } + }) -// The bug: status code will be 403 (from result.Code written first) -// but handleError tries to write 500 (from inferStatusCode) -// This causes inconsistency - status is 403 but error handling expects 500 + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) + + // Should infer 500 from generic error + if rec.Code != 500 { + t.Errorf("expected HTTP status 500, got %d", rec.Code) + } + + var errResp HTTPError + parseJSONResponse(t, rec.Body.Bytes(), &errResp) + if errResp.Code != 500 { + t.Errorf("expected error code 500 in JSON, got %d", errResp.Code) + } + }) + + t.Run("Result with Code and Data - no error", func(t *testing.T) { + handler := H(func() Result[map[string]string] { + return Result[map[string]string]{ + Code: 201, + Data: map[string]string{"status": "created"}, + } + }) -// Current behavior: First WriteHeader(403) succeeds, second is ignored with warning -if rec.Code != 403 { -t.Errorf("expected status 403 (first WriteHeader wins), got %d", rec.Code) -} -}) + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) + + if rec.Code != 201 { + t.Errorf("expected HTTP status 201, got %d", rec.Code) + } + + var resp map[string]string + parseJSONResponse(t, rec.Body.Bytes(), &resp) + if resp["status"] != "created" { + t.Errorf("expected status=created in response") + } + }) } + From 163583f834647856abb911d40ddf1bf9dcb920a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 09:52:01 +0000 Subject: [PATCH 4/5] Add edge case tests for Result handling Co-authored-by: cymoo <7800791+cymoo@users.noreply.github.com> --- edge_cases_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 edge_cases_test.go diff --git a/edge_cases_test.go b/edge_cases_test.go new file mode 100644 index 0000000..25cb88e --- /dev/null +++ b/edge_cases_test.go @@ -0,0 +1,76 @@ +package m + +import ( + "errors" + "net/http/httptest" + "testing" +) + +// TestResultEdgeCases tests edge cases for Result handling +func TestResultEdgeCases(t *testing.T) { + Reset() + + t.Run("Result with zero Code should default to 200", func(t *testing.T) { + handler := H(func() Result[string] { + return Result[string]{ + Code: 0, // Zero value + Data: "test data", + } + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) + + // Should default to 200 OK + if rec.Code != 200 { + t.Errorf("expected HTTP status 200, got %d", rec.Code) + } + }) + + t.Run("Result with negative Code should be handled", func(t *testing.T) { + handler := H(func() Result[string] { + return Result[string]{ + Code: -1, // Invalid negative code + Data: "test", + } + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) + + t.Logf("Response Code: %d", rec.Code) + // Negative codes should be handled - ResponseWriter will convert to 200 + }) + + t.Run("Result with both Data and Err - should prioritize Err", func(t *testing.T) { + handler := H(func() Result[string] { + return Result[string]{ + Code: 500, + Data: "this should not appear", + Err: errors.New("error occurred"), + } + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) + + if rec.Code != 500 { + t.Errorf("expected HTTP status 500, got %d", rec.Code) + } + + // Should return error, not data + if rec.Body.String() == "this should not appear" { + t.Error("should not return data when error is set") + } + + // Should contain error JSON + var httpErr HTTPError + parseJSONResponse(t, rec.Body.Bytes(), &httpErr) + if httpErr.Code != 500 { + t.Errorf("expected error code 500 in JSON, got %d", httpErr.Code) + } + }) +} From 5bc8b8028c453e44bc95fa73972e07d796986aeb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 09:54:17 +0000 Subject: [PATCH 5/5] Add documentation and validation for handleErrorWithCode Co-authored-by: cymoo <7800791+cymoo@users.noreply.github.com> --- edge_cases_test.go | 24 ++++++++++++++++++++++++ mint.go | 12 +++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/edge_cases_test.go b/edge_cases_test.go index 25cb88e..9645934 100644 --- a/edge_cases_test.go +++ b/edge_cases_test.go @@ -73,4 +73,28 @@ func TestResultEdgeCases(t *testing.T) { t.Errorf("expected error code 500 in JSON, got %d", httpErr.Code) } }) + + t.Run("Result with invalid status code and Err - should use inferred code", func(t *testing.T) { + handler := H(func() Result[string] { + return Result[string]{ + Code: 9999, // Invalid status code + Err: errors.New("not found"), + } + }) + + rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/", nil) + handler(rec, req) + + // Should use inferred code (404 from "not found") instead of invalid 9999 + if rec.Code != 404 { + t.Errorf("expected HTTP status 404 (inferred), got %d", rec.Code) + } + + var httpErr HTTPError + parseJSONResponse(t, rec.Body.Bytes(), &httpErr) + if httpErr.Code != 404 { + t.Errorf("expected error code 404 in JSON, got %d", httpErr.Code) + } + }) } diff --git a/mint.go b/mint.go index bfe7dcf..269b580 100644 --- a/mint.go +++ b/mint.go @@ -870,10 +870,15 @@ func handleResult(w http.ResponseWriter, result Result[any]) error { return handleCommonTypes(w, result.Data) } +// handleError handles error responses with automatic status code inference. +// It delegates to handleErrorWithCode with statusCode=0 to use inferred codes. func handleError(w http.ResponseWriter, err error) error { return handleErrorWithCode(w, err, 0) } +// handleErrorWithCode handles error responses with an optional explicit status code. +// If statusCode is 0, the status is inferred from the error type/message. +// If statusCode is provided, it overrides the inferred status code. func handleErrorWithCode(w http.ResponseWriter, err error, statusCode int) error { if errorHandler() != nil { errorHandler()(w, err) @@ -891,8 +896,13 @@ func handleErrorWithCode(w http.ResponseWriter, err error, statusCode int) error } // If an explicit status code is provided, use it instead of the inferred one + // Status codes should be in the valid HTTP range (100-599) if statusCode != 0 { - httpErr.Code = statusCode + if statusCode < 100 || statusCode > 599 { + logger().Printf("warning: invalid HTTP status code %d, using inferred code %d", statusCode, httpErr.Code) + } else { + httpErr.Code = statusCode + } } w.Header().Set("Content-Type", "application/json; charset=utf-8")