Skip to content

SY-4124: Update Freighter (Go) to allow for heterogenous codecs#2271

Open
pjdotson wants to merge 12 commits intorcfrom
sy-4124-update-freighter-go-to-allow-for-heterogenous-encodings
Open

SY-4124: Update Freighter (Go) to allow for heterogenous codecs#2271
pjdotson wants to merge 12 commits intorcfrom
sy-4124-update-freighter-go-to-allow-for-heterogenous-encodings

Conversation

@pjdotson
Copy link
Copy Markdown
Contributor

@pjdotson pjdotson commented Apr 28, 2026

Issue Pull Request

Linear Issue

SY-4124

Description

Update Freighter (Go) to allow for different request / response Codecs. This means that request and response do not have to use the same codec (e.g., different Accepts and Content-Type headers, and some endpoints could allow for multiple request or response codecs if they wanted to accept or be able to output several different types (e.g., exporting YAML or TOML or JSON)

Basic Readiness

  • I have performed a self-review of my code.
  • I have added relevant, automated tests to cover the changes.
  • I have updated documentation to reflect the changes.

Greptile Summary

This PR refactors the Go Freighter HTTP transport to support heterogeneous request/response codecs: unary servers now negotiate the request decoder from Content-Type and the response encoder from Accept independently, while stream servers accept a per-connection codec factory via WithAdditionalCodec. The framer codec's custom resolver is replaced by WithAdditionalCodec(\"application/sy-framer\", ...).

  • P1 — unary server returns 500 for unsupported Content-Type: resolveRequestDecoder's error is returned bare from fiberHandler, causing Fiber to emit a 500 Internal Server Error. The stream server correctly returns 400; this path should return 415 Unsupported Media Type.

Confidence Score: 3/5

Not safe to merge as-is — a P1 regression (500 vs 415) remains unaddressed, and several previously-flagged P1s (nil dereference, missing defaults, Focus label) may also be outstanding.

One new P1 (500 on unsupported Content-Type) plus multiple P1s carried from prior review rounds that do not appear resolved in the current head commit. Combined, these pull the score below the P1 ceiling of 4.

freighter/go/http/unary_server.go (500 vs 415), freighter/go/http/unary_client.go (nil dereference + missing defaults), freighter/go/http/stream_client.go (nil dereference), core/pkg/server/http_test.go (Focus label)

Important Files Changed

Filename Overview
freighter/go/http/codec.go New file cleanly separating Encoder/Decoder/Codec interfaces from the underlying encoding packages and defining the default JSON/MessagePack codec singletons.
freighter/go/http/unary_server.go New unary server with per-request content negotiation; returns bare error (500) from resolveRequestDecoder when Content-Type is unsupported instead of 415, unlike the stream server which returns 400.
freighter/go/http/unary_client.go New unary client with separate Encoder/Decoders; config.New uses a zero-value base so defaults described in doc-comments are never applied, and parseResponseCtx is called before the nil-guard on httpRes.
freighter/go/http/stream_server.go New streaming server with per-connection codec factory support; content-type error handling is correct (400 + message).
freighter/go/http/stream_client.go Updated stream client; parseResponseCtx(res, target) called before the nil-guard when DialContext fails, causing a nil-pointer dereference.
freighter/go/http/router.go Router now returns an error from NewRouter; uses context.TODO() as the long-lived stream root context instead of a caller-supplied or context.Background()-derived context.
core/pkg/server/http_test.go Focus label left on the single spec will cause Ginkgo to skip all other tests in the suite and exit non-zero in CI.
core/pkg/api/http/http.go Mechanical rename from UnaryServer/StreamServer to NewUnaryServer/NewStreamServer; framer codec now registered via WithAdditionalCodec option rather than a custom resolver.
core/pkg/api/http/framer/codec.go Removes the old ContentType() method and CodecResolver pattern; now exposes WithCodec returning a StreamServerOption that registers a per-connection codec factory.

Sequence Diagram

sequenceDiagram
    participant C as Client
    participant US as UnaryServer
    participant SS as StreamServer

    Note over C,US: Unary Request (heterogeneous codecs)
    C->>US: POST /endpoint Content-Type: application/msgpack Accept: application/json
    US->>US: resolveRequestDecoder(Content-Type)
    alt unsupported Content-Type
        US-->>C: 500 Internal Server Error (should be 415)
    end
    US->>US: resolveResponseEncoder(Accept)
    alt no encoder matches Accept
        US-->>C: 406 Not Acceptable
    end
    US->>US: decoder.Decode(body)
    US->>US: handler(ctx, req)
    US->>US: encoder.Encode(res)
    US-->>C: 200 OK Content-Type: application/json

    Note over C,SS: Stream Upgrade (single negotiated codec)
    C->>SS: GET /endpoint Content-Type: application/msgpack WebSocket upgrade
    SS->>SS: resolveStreamCodec(Content-Type)
    alt unsupported Content-Type
        SS-->>C: 400 Bad Request
    end
    SS->>SS: fiberws.New(handleSocket)
    SS-->>C: 101 Switching Protocols
    loop stream lifetime
        C->>SS: WSMessage[RQ]
        SS-->>C: WSMessage[RS]
    end
    SS-->>C: WSMessage[Close]
Loading

Reviews (7): Last reviewed commit: "Reorganize files" | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@pjdotson pjdotson self-assigned this Apr 28, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 68.87255% with 127 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.29%. Comparing base (faa48a8) to head (769d3e0).

Files with missing lines Patch % Lines
freighter/go/http/stream_server.go 66.33% 26 Missing and 8 partials ⚠️
freighter/go/http/unary_server.go 68.57% 18 Missing and 4 partials ⚠️
freighter/go/http/unary_client.go 72.00% 15 Missing and 6 partials ⚠️
freighter/go/http/http.go 66.03% 13 Missing and 5 partials ⚠️
freighter/integration/http/http.go 0.00% 17 Missing ⚠️
freighter/go/http/stream_client.go 81.08% 9 Missing and 5 partials ⚠️
freighter/go/http/router.go 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##               rc    #2271      +/-   ##
==========================================
- Coverage   64.30%   64.29%   -0.01%     
==========================================
  Files        2166     2168       +2     
  Lines      109898   109980      +82     
  Branches     8298     8285      -13     
==========================================
+ Hits        70669    70711      +42     
- Misses      33183    33218      +35     
- Partials     6046     6051       +5     
Flag Coverage Δ
alamos-go 55.25% <ø> (ø)
arc-go 76.85% <ø> (ø)
aspen 67.77% <ø> (ø)
cesium 82.34% <ø> (ø)
client-py 85.91% <ø> (ø)
client-ts 90.18% <ø> (ø)
console 20.42% <ø> (ø)
freighter-go 62.50% <71.86%> (-0.33%) ⬇️
freighter-integration 1.48% <0.00%> (-0.04%) ⬇️
freighter-py 79.96% <ø> (ø)
freighter-ts 73.87% <ø> (ø)
oracle 62.74% <ø> (ø)
pluto 55.74% <ø> (ø)
x-go 81.79% <ø> (-0.08%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread freighter/go/http/unary_client.go
Comment thread freighter/go/http/router.go
…4-update-freighter-go-to-allow-for-heterogenous-encodings
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do all of these types need to be public? If so, all of them need tests. Some seem to be missing public tests.

Comment thread freighter/go/http/codec_test.go Outdated
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/synnaxlabs/x/http"
fhttp "github.com/synnaxlabs/freighter/http"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need an alias? Double check other files in this PR that may not need aliases.

Comment thread freighter/go/http/http.go Outdated
return b
}

type coreConfig struct {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be renamed.

Comment thread freighter/go/http/http.go Outdated

// streamCore is the common functionality implemented by both the client and server
// streams.
type streamCore[I, O freighter.Payload] struct {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this type belong in this file?

Comment thread freighter/go/http/router.go
. "github.com/synnaxlabs/x/testutil"
)

var _ = Describe("Router", func() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we using our gleak utilities from testutil on this test suite? If not, we should be.

// WithAdditionalCodec registers a stream-server codec on top of the default codec list.
// The constructor is invoked once per matching request so the codec may be stateful and
// hold per-stream state.
func WithAdditionalCodec(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If codec already implements the ContentType method how does this make sense? There has to be a cleaner way to do this.

func NewUnaryClient[RQ, RS freighter.Payload](
configs ...UnaryClientConfig,
) (freighter.UnaryClient[RQ, RS], error) {
cfg, err := config.New(UnaryClientConfig{}, configs...)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 NewUnaryClient ignores documented defaults

config.New is called with UnaryClientConfig{} (zero value) as the base, so the defaultUnaryClientConfig that documents Encoder = msgpack.Codec and Decoders = defaultDecoders is never applied. Calling NewUnaryClient[RQ, RS]() with no arguments will always fail validation with "encoder is required" and "at least one decoder is required", despite the field-level doc-comments marking both fields as [OPTIONAL].

Compare with NewStreamClient, which correctly passes defaultStreamClientConfig as the base:

cfg, err := config.New(defaultStreamClientConfig, configs...)
Suggested change
cfg, err := config.New(UnaryClientConfig{}, configs...)
cfg, err := config.New(defaultUnaryClientConfig, configs...)

Comment on lines +151 to +154
httpRes, err := (&http.Client{}).Do(httpReq)
outCtx := parseResponseCtx(httpRes, target)
if err != nil {
return outCtx, err
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Nil pointer dereference when server is unreachable

parseResponseCtx(httpRes, target) is called before the err != nil guard. When http.Client.Do fails with a network-level error (e.g. connection refused), Go's net/http docs guarantee httpRes is nil. parseResponseCtx immediately dereferences c.Header (len(c.Header)), which panics.

The nil check must come first:

httpRes, err := (&http.Client{}).Do(httpReq)
if err != nil {
    return freighter.Context{}, err
}
outCtx := parseResponseCtx(httpRes, target)

Comment on lines +107 to +109
oCtx := parseResponseCtx(res, target)
if err != nil {
return oCtx, err
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Nil pointer dereference when websocket dial fails

Same pattern as in unary_client.go: parseResponseCtx(res, target) is called before the err != nil guard. When ws.Dialer.DialContext cannot establish a TCP connection, res is nil and the call to parseResponseCtx will panic at len(c.Header). The nil check (and the res.StatusCode check at line 111) must come first.

Comment thread core/pkg/server/http_test.go
Comment on lines +88 to +91
func (s *unaryServer[RQ, RS]) fiberHandler(fCtx fiber.Ctx) error {
decoder, err := s.resolveRequestDecoder(fCtx.Get(fiber.HeaderContentType))
if err != nil {
return err
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unsupported Content-Type returns HTTP 500 instead of 415

When resolveRequestDecoder returns an error (unrecognized or missing Content-Type), the error is returned bare from fiberHandler. Fiber maps a non-fiber error to a 500 Internal Server Error. Clients sending an unsupported encoding will see a confusing 500 instead of a 415 Unsupported Media Type.

This is inconsistent with the stream server, which correctly sends a 400 with a plain-text body:

return upgradeCtx.Status(fiber.StatusBadRequest).SendString(err.Error())

The unary handler should mirror that pattern:

Suggested change
func (s *unaryServer[RQ, RS]) fiberHandler(fCtx fiber.Ctx) error {
decoder, err := s.resolveRequestDecoder(fCtx.Get(fiber.HeaderContentType))
if err != nil {
return err
decoder, err := s.resolveRequestDecoder(fCtx.Get(fiber.HeaderContentType))
if err != nil {
return fCtx.Status(fiber.StatusUnsupportedMediaType).SendString(err.Error())
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants