Skip to content
Draft
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
100 changes: 100 additions & 0 deletions edge_cases_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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)
}
})

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)
}
})
}
31 changes: 27 additions & 4 deletions mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -855,18 +855,31 @@ 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)
}

// 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)
return nil
Expand All @@ -882,6 +895,16 @@ func handleError(w http.ResponseWriter, err error) error {
return nil
}

// 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 {
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")

if !statusWritten {
Expand Down
83 changes: 83 additions & 0 deletions mint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1966,3 +1966,86 @@ func TestCompleteConfigurationScenario(t *testing.T) {
}
})
}

// ========== 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)

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"),
}
})

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"},
}
})

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")
}
})
}