From a6f94558ef123dfa823968b75e1b67ea3ad0d181 Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Tue, 12 May 2026 20:01:49 -0500 Subject: [PATCH 1/6] Add `ErrorEnvelope` struct and `HandleHttpError` utility with tests - Implement reusable `ErrorEnvelope` struct for standardized error responses. - Add `HandleHttpError` function to write error details as `application/problem+json`. - Include unit tests to validate behavior with various scenarios. --- internal/error_envelope.go | 28 +++++++++++ internal/error_envelope_test.go | 83 +++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 internal/error_envelope.go create mode 100644 internal/error_envelope_test.go diff --git a/internal/error_envelope.go b/internal/error_envelope.go new file mode 100644 index 0000000..dcc8121 --- /dev/null +++ b/internal/error_envelope.go @@ -0,0 +1,28 @@ +package internal + +import ( + "encoding/json" + "net/http" +) + +type ErrorEnvelope struct { + Type string `json:"type"` + Title string `json:"title"` + Status int `json:"status,omitzero"` + Detail string `json:"detail,omitzero"` + Instance string `json:"instance,omitzero"` +} + +func HandleHttpError(w http.ResponseWriter, err ErrorEnvelope, statusCode int) { + if err.Type == "" { + err.Type = "about:blank" + } + if err.Title == "" { + err.Title = http.StatusText(statusCode) + } + err.Status = statusCode + + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(statusCode) + _ = json.NewEncoder(w).Encode(err) +} diff --git a/internal/error_envelope_test.go b/internal/error_envelope_test.go new file mode 100644 index 0000000..60d0a21 --- /dev/null +++ b/internal/error_envelope_test.go @@ -0,0 +1,83 @@ +package internal + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHandleHttpError(t *testing.T) { + tests := []struct { + name string + err ErrorEnvelope + statusCode int + expectedStatus int + expectedBody string + expectedTitle string + expectedType string + }{ + { + name: "Empty Envelope", + err: ErrorEnvelope{}, + statusCode: http.StatusBadRequest, + expectedStatus: http.StatusBadRequest, + expectedTitle: "Bad Request", + expectedType: "about:blank", + }, + { + name: "With Detail", + err: ErrorEnvelope{ + Detail: "Validation failed", + }, + statusCode: http.StatusUnprocessableEntity, + expectedStatus: http.StatusUnprocessableEntity, + expectedTitle: "Unprocessable Entity", + expectedType: "about:blank", + }, + { + name: "Custom Type and Title", + err: ErrorEnvelope{ + Type: "https://example.com/probs/out-of-credit", + Title: "You do not have enough credit.", + }, + statusCode: http.StatusForbidden, + expectedStatus: http.StatusForbidden, + expectedTitle: "You do not have enough credit.", + expectedType: "https://example.com/probs/out-of-credit", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rr := httptest.NewRecorder() + HandleHttpError(rr, tt.err, tt.statusCode) + + if rr.Code != tt.expectedStatus { + t.Errorf("expected status %d, got %d", tt.expectedStatus, rr.Code) + } + + contentType := rr.Header().Get("Content-Type") + if contentType != "application/problem+json" { + t.Errorf("expected Content-Type application/problem+json, got %q", contentType) + } + + var got ErrorEnvelope + if err := json.NewDecoder(rr.Body).Decode(&got); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + + if got.Status != tt.expectedStatus { + t.Errorf("expected envelope status %d, got %d", tt.expectedStatus, got.Status) + } + + if got.Title != tt.expectedTitle { + t.Errorf("expected envelope title %q, got %q", tt.expectedTitle, got.Title) + } + + if got.Type != tt.expectedType { + t.Errorf("expected envelope type %q, got %q", tt.expectedType, got.Type) + } + }) + } +} From 66bf41576609bcd2a6537a83b264bedd9121b2e7 Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Tue, 12 May 2026 20:02:08 -0500 Subject: [PATCH 2/6] Refactor error handling across product and platform handlers using `HandleHttpError` for standardized responses. Update tests to validate error structure. --- internal/platform/handler.go | 36 +++++++++++++++---------------- internal/platform/handler_test.go | 15 ++++++++++--- internal/product/handler.go | 36 +++++++++++++++---------------- 3 files changed, 48 insertions(+), 39 deletions(-) diff --git a/internal/platform/handler.go b/internal/platform/handler.go index b84e35d..4e8c661 100644 --- a/internal/platform/handler.go +++ b/internal/platform/handler.go @@ -27,18 +27,18 @@ type platformHandler struct { func (h *platformHandler) CreatePlatform(w http.ResponseWriter, r *http.Request) { var req createPlatformRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid request body"}, http.StatusBadRequest) return } if req.Name == "" { - http.Error(w, "Name is required", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Name is required"}, http.StatusBadRequest) return } contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() if err := h.queries.CreatePlatform(contextWithTimeOut, req.ToParams()); err != nil { - http.Error(w, "Failed to create platform", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to create platform"}, http.StatusInternalServerError) return } @@ -48,22 +48,22 @@ func (h *platformHandler) CreatePlatform(w http.ResponseWriter, r *http.Request) func (h *platformHandler) UpdatePlatform(w http.ResponseWriter, r *http.Request) { var req updatePlatformRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid request body"}, http.StatusBadRequest) return } id, ok := internal.GetIntFromRequestPath("id", r) if !ok { - http.Error(w, "Invalid platform ID", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid platform ID"}, http.StatusBadRequest) return } if req.Name == "" { - http.Error(w, "Name is required", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Name is required"}, http.StatusBadRequest) return } if req.ID != id { - http.Error(w, "Platform ID does not match path", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Platform ID does not match path"}, http.StatusBadRequest) return } contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) @@ -71,10 +71,10 @@ func (h *platformHandler) UpdatePlatform(w http.ResponseWriter, r *http.Request) _, err := h.queries.UpdatePlatform(contextWithTimeOut, req.ToParams(id)) if err != nil { if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "Platform not found", http.StatusNotFound) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Platform not found"}, http.StatusNotFound) return } - http.Error(w, "Failed to update platform", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to update platform"}, http.StatusInternalServerError) return } @@ -84,7 +84,7 @@ func (h *platformHandler) UpdatePlatform(w http.ResponseWriter, r *http.Request) func (h *platformHandler) DeletePlatform(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { - http.Error(w, "Invalid platform ID", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid platform ID"}, http.StatusBadRequest) return } contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) @@ -92,12 +92,12 @@ func (h *platformHandler) DeletePlatform(w http.ResponseWriter, r *http.Request) _, err := h.queries.DeletePlatform(contextWithTimeOut, id) if err != nil { if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "Platform not found", http.StatusNotFound) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Platform not found"}, http.StatusNotFound) return } logger := internal.LoggerFromContext(r.Context()) logger.Error("Failed to delete platform", "error", err, "platform_id", id) - http.Error(w, "Internal server error", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Internal server error"}, http.StatusInternalServerError) return } @@ -109,7 +109,7 @@ func (h *platformHandler) GetPlatforms(w http.ResponseWriter, r *http.Request) { defer cancel() platforms, err := h.queries.GetPlatforms(contextWithTimeOut) if err != nil { - http.Error(w, "Failed to fetch platforms", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to fetch platforms"}, http.StatusInternalServerError) return } if platforms == nil { @@ -118,7 +118,7 @@ func (h *platformHandler) GetPlatforms(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(platforms) if err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to encode response"}, http.StatusInternalServerError) return } @@ -127,7 +127,7 @@ func (h *platformHandler) GetPlatforms(w http.ResponseWriter, r *http.Request) { func (h *platformHandler) GetPlatform(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { - http.Error(w, "Invalid platform ID", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid platform ID"}, http.StatusBadRequest) return } contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) @@ -135,17 +135,17 @@ func (h *platformHandler) GetPlatform(w http.ResponseWriter, r *http.Request) { platform, err := h.queries.GetPlatform(contextWithTimeOut, id) if err != nil { if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "Platform not found", http.StatusNotFound) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Platform not found"}, http.StatusNotFound) return } - http.Error(w, "Failed to fetch platform", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to fetch platform"}, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(platform) if err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to encode response"}, http.StatusInternalServerError) return } } diff --git a/internal/platform/handler_test.go b/internal/platform/handler_test.go index 03d3d85..0fa8de6 100644 --- a/internal/platform/handler_test.go +++ b/internal/platform/handler_test.go @@ -7,6 +7,7 @@ import ( "errors" "net/http" "net/http/httptest" + "products/internal" "strconv" "testing" @@ -298,14 +299,14 @@ func TestDeletePlatform(t *testing.T) { id: "1", dbErr: errors.New("db error"), expectedStatus: http.StatusInternalServerError, - expectedBody: "Internal server error\n", + expectedBody: "Internal server error", }, { name: "DB Error (pgx.ErrNoRows)", id: "1", dbErr: pgx.ErrNoRows, expectedStatus: http.StatusNotFound, - expectedBody: "Platform not found\n", + expectedBody: "Platform not found", }, { name: "Invalid ID (Zero)", @@ -351,7 +352,15 @@ func TestDeletePlatform(t *testing.T) { } if tt.expectedBody != "" { - if rr.Body.String() != tt.expectedBody { + if rr.Code >= 400 { + var envelope internal.ErrorEnvelope + if err := json.NewDecoder(rr.Body).Decode(&envelope); err != nil { + t.Fatalf("failed to decode error response: %v", err) + } + if envelope.Detail != tt.expectedBody { + t.Errorf("expected detail %q, got %q", tt.expectedBody, envelope.Detail) + } + } else if rr.Body.String() != tt.expectedBody { t.Errorf("expected body %q, got %q", tt.expectedBody, rr.Body.String()) } } diff --git a/internal/product/handler.go b/internal/product/handler.go index 95dbb8b..ff22d85 100644 --- a/internal/product/handler.go +++ b/internal/product/handler.go @@ -28,13 +28,13 @@ type productHandler struct { func (h *productHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { var req createProductRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid request body"}, http.StatusBadRequest) return } req.Name = strings.TrimSpace(req.Name) if req.Name == "" || req.PlatformID == 0 { - http.Error(w, "Name and platform ID are required", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Name and platform ID are required"}, http.StatusBadRequest) return } @@ -42,7 +42,7 @@ func (h *productHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { defer cancel() if err := h.queries.CreateProduct(contextWithTimeOut, req.ToParams()); err != nil { - http.Error(w, "Failed to create product", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to create product"}, http.StatusInternalServerError) return } @@ -52,17 +52,17 @@ func (h *productHandler) CreateProduct(w http.ResponseWriter, r *http.Request) { func (h *productHandler) DeleteProduct(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { - http.Error(w, "Invalid product ID", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid product ID"}, http.StatusBadRequest) return } contextWithTimeOut, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() if _, err := h.queries.DeleteProduct(contextWithTimeOut, id); err != nil { if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "Product not found", http.StatusNotFound) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Product not found"}, http.StatusNotFound) return } - http.Error(w, "Failed to delete product", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to delete product"}, http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) @@ -71,19 +71,19 @@ func (h *productHandler) DeleteProduct(w http.ResponseWriter, r *http.Request) { func (h *productHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { - http.Error(w, "Invalid product ID", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid product ID"}, http.StatusBadRequest) return } var req updateProductRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid request body"}, http.StatusBadRequest) return } req.Name = strings.TrimSpace(req.Name) if req.Name == "" || req.PlatformID == 0 { - http.Error(w, "Name and platform ID are required", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Name and platform ID are required"}, http.StatusBadRequest) return } @@ -92,10 +92,10 @@ func (h *productHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) { if _, err := h.queries.UpdateProduct(ctx, req.ToParams(id)); err != nil { if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "Product not found", http.StatusNotFound) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Product not found"}, http.StatusNotFound) return } - http.Error(w, "Failed to update product", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to update product"}, http.StatusInternalServerError) return } @@ -106,7 +106,7 @@ func (h *productHandler) UpdateProduct(w http.ResponseWriter, r *http.Request) { func (h *productHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Request) { platformID, ok := internal.GetIntFromRequestPath("platform_id", r) if !ok { - http.Error(w, "Invalid platform ID", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid platform ID"}, http.StatusBadRequest) return } @@ -115,7 +115,7 @@ func (h *productHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Re products, err := h.queries.GetProductsByPlatform(ctx, platformID) if err != nil { - http.Error(w, "Failed to fetch products", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to fetch products"}, http.StatusInternalServerError) return } @@ -125,7 +125,7 @@ func (h *productHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Re w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(products); err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to encode response"}, http.StatusInternalServerError) return } } @@ -134,7 +134,7 @@ func (h *productHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Re func (h *productHandler) GetProductById(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { - http.Error(w, "Invalid product ID", http.StatusBadRequest) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Invalid product ID"}, http.StatusBadRequest) return } @@ -144,16 +144,16 @@ func (h *productHandler) GetProductById(w http.ResponseWriter, r *http.Request) product, err := h.queries.GetProductById(ctx, id) if err != nil { if errors.Is(err, pgx.ErrNoRows) { - http.Error(w, "Product not found", http.StatusNotFound) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Product not found"}, http.StatusNotFound) return } - http.Error(w, "Failed to fetch product", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to fetch product"}, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(product); err != nil { - http.Error(w, "Failed to encode response", http.StatusInternalServerError) + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to encode response"}, http.StatusInternalServerError) return } } From 3c2715cab4706d5570dee9cd83cd37989e316585 Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Tue, 12 May 2026 20:17:00 -0500 Subject: [PATCH 3/6] Improve `HandleHttpError` to default invalid status codes to 500 and log unknown codes. Update tests with additional cases for zero and negative status codes. --- internal/error_envelope.go | 19 +++++++++++++++---- internal/error_envelope_test.go | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/internal/error_envelope.go b/internal/error_envelope.go index dcc8121..0f2b521 100644 --- a/internal/error_envelope.go +++ b/internal/error_envelope.go @@ -2,21 +2,29 @@ package internal import ( "encoding/json" + "log/slog" "net/http" ) type ErrorEnvelope struct { Type string `json:"type"` Title string `json:"title"` - Status int `json:"status,omitzero"` - Detail string `json:"detail,omitzero"` - Instance string `json:"instance,omitzero"` + Status int `json:"status"` + Detail string `json:"detail,omitempty"` + Instance string `json:"instance,omitempty"` } +// HandleHttpError handles an error and writes it to the response writer +// in the format specified by RFC 7807. If an invalid status code is provided, +// it will default to 500 Internal Server Error. func HandleHttpError(w http.ResponseWriter, err ErrorEnvelope, statusCode int) { if err.Type == "" { err.Type = "about:blank" } + if http.StatusText(statusCode) == "" { + slog.Error("Unknown HTTP status code", "status_code", statusCode) + statusCode = http.StatusInternalServerError + } if err.Title == "" { err.Title = http.StatusText(statusCode) } @@ -24,5 +32,8 @@ func HandleHttpError(w http.ResponseWriter, err ErrorEnvelope, statusCode int) { w.Header().Set("Content-Type", "application/problem+json") w.WriteHeader(statusCode) - _ = json.NewEncoder(w).Encode(err) + + if encodeErr := json.NewEncoder(w).Encode(err); encodeErr != nil { + slog.Error("Failed to encode error response", "error", encodeErr) + } } diff --git a/internal/error_envelope_test.go b/internal/error_envelope_test.go index 60d0a21..83494ad 100644 --- a/internal/error_envelope_test.go +++ b/internal/error_envelope_test.go @@ -46,6 +46,22 @@ func TestHandleHttpError(t *testing.T) { expectedTitle: "You do not have enough credit.", expectedType: "https://example.com/probs/out-of-credit", }, + { + name: "Zero Status Code Defaults to 500", + err: ErrorEnvelope{Detail: "Something went wrong"}, + statusCode: 0, + expectedStatus: http.StatusInternalServerError, + expectedTitle: "Internal Server Error", + expectedType: "about:blank", + }, + { + name: "Negative Status Code Defaults to 500", + err: ErrorEnvelope{Detail: "Something went wrong"}, + statusCode: -1, + expectedStatus: http.StatusInternalServerError, + expectedTitle: "Internal Server Error", + expectedType: "about:blank", + }, } for _, tt := range tests { From 1c435d007c6d45d26fc6fb8d755a3bdae66dce6c Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Wed, 13 May 2026 20:23:11 -0500 Subject: [PATCH 4/6] chore: adds junie folder to gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9725b60..12c8503 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,6 @@ coverage/ # Liquibase Specific files/folders liquibase.properties -liquibase_libs/ \ No newline at end of file +liquibase_libs/ + +.junie \ No newline at end of file From 2a9f77e8b2e9c57699f12ffdf95f47064db109ee Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Wed, 13 May 2026 20:23:26 -0500 Subject: [PATCH 5/6] Refactor response encoding in `product` and `platform` handlers to use a `bytes.Buffer` for improved reliability --- internal/platform/handler.go | 14 ++++++++++---- internal/product/handler.go | 15 +++++++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/internal/platform/handler.go b/internal/platform/handler.go index 4e8c661..0b860b9 100644 --- a/internal/platform/handler.go +++ b/internal/platform/handler.go @@ -1,6 +1,7 @@ package platform import ( + "bytes" "context" "encoding/json" "errors" @@ -115,13 +116,15 @@ func (h *platformHandler) GetPlatforms(w http.ResponseWriter, r *http.Request) { if platforms == nil { platforms = []Platform{} } - w.Header().Set("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(platforms) + var buf bytes.Buffer + err = json.NewEncoder(&buf).Encode(platforms) if err != nil { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to encode response"}, http.StatusInternalServerError) return } + w.Header().Set("Content-Type", "application/json") + w.Write(buf.Bytes()) } func (h *platformHandler) GetPlatform(w http.ResponseWriter, r *http.Request) { @@ -142,10 +145,13 @@ func (h *platformHandler) GetPlatform(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", "application/json") - err = json.NewEncoder(w).Encode(platform) + var buf bytes.Buffer + err = json.NewEncoder(&buf).Encode(platform) if err != nil { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to encode response"}, http.StatusInternalServerError) return } + + w.Header().Set("Content-Type", "application/json") + w.Write(buf.Bytes()) } diff --git a/internal/product/handler.go b/internal/product/handler.go index ff22d85..7fb9160 100644 --- a/internal/product/handler.go +++ b/internal/product/handler.go @@ -1,6 +1,7 @@ package product import ( + "bytes" "context" "encoding/json" "errors" @@ -123,11 +124,14 @@ func (h *productHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Re products = []Product{} } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(products); err != nil { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(products); err != nil { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to encode response"}, http.StatusInternalServerError) return } + + w.Header().Set("Content-Type", "application/json") + w.Write(buf.Bytes()) } // GetProductById fetches a single product by ID. @@ -151,9 +155,12 @@ func (h *productHandler) GetProductById(w http.ResponseWriter, r *http.Request) return } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(product); err != nil { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(product); err != nil { internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to encode response"}, http.StatusInternalServerError) return } + + w.Header().Set("Content-Type", "application/json") + w.Write(buf.Bytes()) } From 39a0feccae6b0705c3d665333dbca458551ae649 Mon Sep 17 00:00:00 2001 From: Josh Potts <9337140+jlpdeveloper@users.noreply.github.com> Date: Thu, 14 May 2026 19:26:16 -0500 Subject: [PATCH 6/6] Improve error handling when writing JSON responses in `product` handlers using `HandleHttpError` for consistent error reporting --- internal/product/handler.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/product/handler.go b/internal/product/handler.go index 7fb9160..318979d 100644 --- a/internal/product/handler.go +++ b/internal/product/handler.go @@ -131,10 +131,10 @@ func (h *productHandler) GetProductsByPlatform(w http.ResponseWriter, r *http.Re } w.Header().Set("Content-Type", "application/json") - w.Write(buf.Bytes()) + if _, err := w.Write(buf.Bytes()); err != nil { + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to write response"}, http.StatusInternalServerError) + } } - -// GetProductById fetches a single product by ID. func (h *productHandler) GetProductById(w http.ResponseWriter, r *http.Request) { id, ok := internal.GetIntFromRequestPath("id", r) if !ok { @@ -162,5 +162,7 @@ func (h *productHandler) GetProductById(w http.ResponseWriter, r *http.Request) } w.Header().Set("Content-Type", "application/json") - w.Write(buf.Bytes()) + if _, err := w.Write(buf.Bytes()); err != nil { + internal.HandleHttpError(w, internal.ErrorEnvelope{Detail: "Failed to write response"}, http.StatusInternalServerError) + } }