Fix two server correctness issues identified during research: (1) the server does not set Content-Type response headers, and (2) the marshalResponse function rejects unknown content types instead of defaulting to JSON.

Purpose: The Go HTTP server is the source of truth for "what clients should expect." If the server doesn't set Content-Type on responses, clients cannot reliably determine the response format. If the server rejects unknown content types on response serialization, it creates an asymmetry with request parsing (which defaults to binary). Both issues must be fixed to establish a correct server baseline before auditing clients.

Output: Corrected server generator code with proper Content-Type response headers and consistent JSON default behavior. Updated golden files.

<execution_context> @/Users/sebastienmelki/.claude/get-shit-done/workflows/execute-plan.md @/Users/sebastienmelki/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-existing-client-review/03-CONTEXT.md @.planning/phases/03-existing-client-review/03-RESEARCH.md @internal/httpgen/generator.go Task 1: Fix Content-Type response header and marshalResponse default behavior internal/httpgen/generator.go Two fixes in the generated server code:

Fix 1: Set Content-Type on all responses

In generateWriteProtoMessageResponseFunc (around line 860-893), the generated writeProtoMessageResponse function writes response bytes but never sets the Content-Type header. Fix this by adding w.Header().Set("Content-Type", ...) BEFORE w.WriteHeader(statusCode) (headers must be set before WriteHeader is called in Go's net/http).

The Content-Type should match the serialization format:

  • JSON path: w.Header().Set("Content-Type", "application/json")
  • Binary/Proto path: w.Header().Set("Content-Type", "application/x-protobuf")
  • Default path (already defaults to JSON): w.Header().Set("Content-Type", "application/json")

Restructure the generated code so the Content-Type header is set inside the switch cases (or set a variable and set it after the switch but before WriteHeader). The cleanest approach:

var responseBytes []byte
var err error
var respContentType string
 
switch filterFlags(contentType) {
case JSONContentType:
    responseBytes, err = protojson.Marshal(msg)
    respContentType = "application/json"
case BinaryContentType, ProtoContentType:
    responseBytes, err = proto.Marshal(msg)
    respContentType = "application/x-protobuf"
default:
    responseBytes, err = protojson.Marshal(msg)
    respContentType = "application/json"
}
 
if err != nil {
    http.Error(w, fallbackMsg, statusCode)
    return
}
 
w.Header().Set("Content-Type", respContentType)
w.WriteHeader(statusCode)
_, _ = w.Write(responseBytes)

Fix 2: marshalResponse should default to JSON for unknown content types

In generateBindingHelpers (around line 629-653), the generated marshalResponse function has a default case that returns an error: return nil, fmt.Errorf("unsupported content type: %s", contentType). This is inconsistent with the writeProtoMessageResponse function which already defaults to JSON.

Change the default case in marshalResponse to default to JSON (matching protojson as source of truth):

default:
    // Default to JSON for unrecognized content types
    if marshaler, ok := response.(json.Marshaler); ok {
        return marshaler.MarshalJSON()
    }
    return protojson.Marshal(msg)

Also set the response Content-Type header in the handler function that calls marshalResponse. Look for where marshalResponse is called (in the generated handler functions) and add w.Header().Set("Content-Type", ...) there too. Find the generateMethodHandler or equivalent function that calls marshalResponse and writes the response. The Content-Type should be determined from the request Content-Type (matching the serialization format used) and set on the response.

Actually, check the full flow: marshalResponse returns bytes but doesn't have access to w. The caller writes the bytes. Find where the caller writes and add Content-Type there. The pattern is likely:

responseBytes, err := marshalResponse(r, response)
// ... error handling ...
w.WriteHeader(http.StatusOK)
w.Write(responseBytes)

Add Content-Type setting before WriteHeader. Determine the Content-Type by checking r.Header.Get("Content-Type") (same logic as marshalResponse) and set it on the response:

respContentType := "application/json"
if ct := filterFlags(r.Header.Get("Content-Type")); ct == BinaryContentType || ct == ProtoContentType {
    respContentType = "application/x-protobuf"
}
w.Header().Set("Content-Type", respContentType)

IMPORTANT: Do NOT change field names, function signatures, or break any public API. These are internal generated helper functions. The fix only adds Content-Type header setting and changes the default case from error to JSON. Run make build to verify compilation. Run UPDATE_GOLDEN=1 go test ./internal/httpgen/ -run TestExhaustiveGoldenFiles -count=1 to update golden files. Run go test ./internal/httpgen/ -count=1 to confirm all tests pass. Inspect the golden file diff to verify Content-Type headers are set and the default case is changed. Every generated HTTP server response includes a Content-Type header matching the serialization format (application/json or application/x-protobuf). Unknown request content types default to JSON serialization instead of returning an error. Golden files updated to reflect the changes.

Task 2: Verify error response functions also set Content-Type headers internal/httpgen/generator.go Audit ALL error response paths in the generated server code to ensure they also set Content-Type. The `writeProtoMessageResponse` function was fixed in Task 1, but verify these other error paths:
  1. writeValidationErrorResponse (around line 896) -- This should call writeProtoMessageResponse which now sets Content-Type. Verify.
  2. writeErrorResponse -- Same, should delegate to writeProtoMessageResponse. Verify.
  3. writeResponseBody -- This is the main response writing function. Verify it sets Content-Type.
  4. writeErrorWithHandler -- Custom error handler path. Verify Content-Type is set.
  5. Any direct http.Error() calls -- These use text/plain by default which is correct for fallback errors.
  6. Header validation error responses -- Check generateHeaderValidationFunctions for Content-Type setting.

For each function:

  • If it delegates to writeProtoMessageResponse, it's already fixed (Task 1).
  • If it writes bytes directly, add Content-Type setting.
  • If it uses http.Error(), that's fine (sets text/plain automatically).

Also verify that the generateWriteResponseBodyFunc (the main happy-path response writer) sets Content-Type before writing. This is the function called for successful RPC responses.

If any path is missing Content-Type, fix it following the same pattern as Task 1.

After fixes, run make lint-fix to clean up any formatting issues. Run go test ./internal/httpgen/ -count=1 to confirm all tests pass. Grep the generated golden files for Content-Type to verify it appears in all response paths: grep -n "Content-Type" internal/httpgen/testdata/golden/*_binding.pb.go. Every function that writes an HTTP response should set Content-Type. All generated server response paths (success responses, validation errors, handler errors, header validation errors) set the Content-Type response header. No response path writes bytes without first setting Content-Type.

1. `go test ./internal/httpgen/ -count=1` passes 2. `make build` succeeds 3. Golden file diffs show Content-Type headers added to all response paths 4. `grep "Content-Type" internal/httpgen/testdata/golden/*_binding.pb.go` shows Content-Type in all binding files 5. marshalResponse default case returns JSON instead of error

<success_criteria>

  • Every generated HTTP server response includes a correct Content-Type header
  • Unknown/empty request Content-Type defaults to JSON for both request parsing and response serialization
  • The server behavior is internally consistent (no asymmetry between request and response content-type handling)
  • All httpgen tests pass with updated golden files </success_criteria>
After completion, create `.planning/phases/03-existing-client-review/03-02-SUMMARY.md`