Skip to content

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

Open
pjdotson wants to merge 14 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 14 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 Freighter Go HTTP transport to support heterogeneous request/response codecs, allowing the request Content-Type and response Accept header to specify different encodings independently. The old single-codec-per-connection model is replaced with separate encoder/decoder registries on both the unary server and client.

  • x/go/http/codec.go is removed and its role is absorbed into the new freighter/go/http/codec.go, which defines Encoder, Decoder, and Codec interfaces along with JSONCodec, MsgPackCodec, and shared default registries; ContentType() is removed from the underlying x/encoding/json and x/encoding/msgpack packages.
  • The monolithic stream.go and unary.go are split into focused files; NewRouter now returns an error instead of panicking; stream servers gain WithAdditionalCodec for per-connection stateful codecs (used by the framer).
  • A substantial test suite is added in router_test.go and unary_test.go, covering q-value ordering, wildcard Accept, 406 on no match, and cross-codec round-trips.

Confidence Score: 4/5

The core codec-splitting logic and new content-negotiation path are well-tested, but several defects in the client and server error paths from the previous review round remain unaddressed.

The unary and stream clients call parseResponseCtx before guarding against a nil response causing a panic on network failure, NewUnaryClient silently fails validation with no arguments despite [OPTIONAL] docs, the unary server returns HTTP 500 instead of 415 for unrecognised Content-Type, and the Focus label in http_test.go will break CI. The framer codec integration, encoder/decoder separation, and router lifecycle refactor are all clean.

freighter/go/http/unary_client.go and freighter/go/http/http.go (nil dereference on network failure), freighter/go/http/unary_server.go (wrong HTTP status for bad Content-Type), core/pkg/server/http_test.go (Focus label).

Important Files Changed

Filename Overview
freighter/go/http/codec.go New file introducing Encoder/Decoder/Codec interfaces and default codec registries. Package-level vars are IIFE-initialized and treated as read-only; safe.
freighter/go/http/unary_server.go New unary server with heterogeneous request decoder / response encoder. A bare error return from resolveRequestDecoder propagates as HTTP 500 rather than 415.
freighter/go/http/unary_client.go New unary client with per-direction encoder/decoder. parseResponseCtx is called before the nil-guard on httpRes; NewUnaryClient bases config.New on a zero-value struct so calling it with no args silently fails validation despite [OPTIONAL] docs.
freighter/go/http/stream_client.go New stream client with configurable codec. parseResponseCtx called before nil-guard on the dial response; otherwise well-structured refactor.
freighter/go/http/stream_server.go New stream server with per-connection codec resolution including additional stateful codecs. The factory pattern cleanly handles the framer use case.
freighter/go/http/router.go NewRouter now returns an error instead of panicking. Uses context.TODO() as root context for stream lifecycle; constructors renamed to NewStreamServer/NewUnaryServer.
freighter/go/http/http.go parseResponseCtx moved here; the nil-guard on c *http.Response is absent, so a nil dereference occurs on network failure before the error is checked.
core/pkg/server/http_test.go Test expanded with proper round-trip assertions and dynamic port allocation, but the Focus label is still present and will cause Ginkgo to skip all other specs and fail CI.
freighter/go/http/router_test.go New comprehensive router test covering config validation, BindTo lifecycle, middleware ordering, and stream cancellation on shutdown.
freighter/go/http/unary_test.go Extended with a thorough content-negotiation suite covering cross-codec round trips, q-value ordering, wildcard Accept, 406 on no match, and error encoding.
core/pkg/api/http/framer/codec.go ContentType() removed from framer Codec; replaced with WithCodec(channelSvc) returning a StreamServerOption that registers the per-connection framer via WithAdditionalCodec.
x/go/encoding/json/json.go ContentType() removed; Codec var type widened to encoding.Codec. Clean decoupling of HTTP concerns from the encoding package.
x/go/encoding/msgpack/msgpack.go ContentType() removed, Codec type widened to encoding.Codec.

Reviews (9): Last reviewed commit: "Merge branch 'rc' into sy-4124-update-fr..." | Re-trigger Greptile

@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.81%. Comparing base (c6358eb) to head (b92358d).

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.83%   64.81%   -0.02%     
==========================================
  Files        2582     2584       +2     
  Lines      111582   111617      +35     
  Branches     8321     8309      -12     
==========================================
+ Hits        72345    72348       +3     
- Misses      33173    33206      +33     
+ Partials     6064     6063       -1     
Flag Coverage Δ
alamos-go 55.25% <ø> (ø)
arc-go 76.93% <ø> (ø)
aspen 68.01% <ø> (+0.23%) ⬆️
cesium 82.31% <ø> (-0.03%) ⬇️
client-py 85.91% <ø> (-0.03%) ⬇️
client-ts 90.18% <ø> (ø)
console 21.65% <ø> (ø)
freighter-go 62.58% <71.86%> (-0.42%) ⬇️
freighter-integration 1.48% <0.00%> (-0.04%) ⬇️
freighter-py 79.96% <ø> (ø)
freighter-ts 73.87% <ø> (ø)
oracle 62.94% <ø> (ø)
pluto 58.42% <ø> (-0.05%) ⬇️

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