Skip to content

feat: add a Go SDK alongside the Python and Node.js ports - #119

Merged
JeanExtreme002 merged 14 commits into
mainfrom
feat/go-sdk
Aug 25, 2026
Merged

feat: add a Go SDK alongside the Python and Node.js ports#119
JeanExtreme002 merged 14 commits into
mainfrom
feat/go-sdk

Conversation

@JeanExtreme002

Copy link
Copy Markdown
Owner

Adds a third SDK under go/, importable as
github.com/JeanExtreme002/FlightRadarAPI/go/flightradarapi.

Same features, Go-shaped surface

Every method of FlightRadar24API has a counterpart, with the same
parameters. What differs is the shape:

Python / Node.js Go
new FlightRadar24API({timeout, maxWorkers}) New(Options{Timeout: ..., MaxWorkers: ...})
get_flights(airline, bounds, ...) GetFlights(ctx, FlightSearch{...})
check_info(min_altitude=6700) CheckInfo(map[string]any{"min_altitude": 6700})
Countries.BRAZIL CountryBrazil, with AllCountries() to enumerate
default arguments (flight_limit=100) zero means the same default
exceptions error values wrapping ErrFlightRadar
blocking calls context.Context on every request

The package doc opens with this mapping, so someone arriving from the
Python or Node.js docs finds where each name went.

Parity is enforced, not promised

ports_test.go reads the Python source and fails when the two drift:

  • the 228 Country constants against the Countries enum
  • the bundled zones against zones.py
  • FlightTrackerConfig against the dataclass
  • every attribute Flight exposes, so a ported check_info filter keeps working
  • every public method of FlightRadar24API, by reflection

countries.go and zones.go are generated from the Python source and
say so in their header.

Written from scratch where the sibling ports lean on their HTTP client

  • Cookie jar honouring the scope FR24 sets: a cookie stored by www.
    is not replayed to cdn./api./data-live., Path/Secure/expiry
    are respected, cookies handed out on a redirect hop are banked, and a
    single cookie can be dropped to shed load-balancer stickiness without
    losing the login.
  • Content decoding owned by the package (gzip incl. multi-member,
    deflate in both shapes, brotli), which is what makes the size budget
    enforceable before a body expands rather than after.

Testing

214 tests, 93% coverage, race detector clean. The offline suite gates
PRs; a live FR24 suite runs behind -tags integration and passes against
production. gofmt, go vet and staticcheck stand in for the linters
the other two ports run.

Releasing

Go needs no registry upload — the tag is the release. Because the module
lives in a subdirectory, the toolchain only sees tags carrying that
prefix, so publish.yml now pushes go/vX.Y.Z alongside the release
tag, and the version check covers all three ports.

One open question

The live FR24 step keeps the continue-on-error: ${{ github.event_name == 'push' }} line copied from the Python and Node.js workflows, which makes
that suite a hard gate on pull requests. An FR24 outage can therefore fail
a docs-only PR. Changing it is a repo-wide policy call, so it is left as
is here rather than making the Go workflow the odd one out.

The port carries the same features as the other two, with the surface
shaped for Go: a context on every request, errors instead of exceptions,
Options instead of keyword arguments, and a zero value that means the
default the other ports declare in their signatures.

Two pieces have no counterpart in the sibling ports, because their HTTP
clients provide them:

- a cookie jar that honours the scope FR24 sets, so a cookie stored by
  www. is not replayed to cdn./api./data-live., and a single cookie can
  be dropped to shed load-balancer stickiness without losing the login
- content decoding owned by the package (gzip, deflate, brotli), which
  is what makes the size budget enforceable before a body expands

Parity is enforced rather than promised: ports_test.go reads the Python
source and fails when the country list, the zones, the tracker config,
the flight attributes or the client methods drift apart.
The offline suite gates pull requests and the live FR24 suite runs with
retries, mirroring the Python and Node.js workflows. gofmt, go vet and
staticcheck stand in for flake8/mypy and eslint/tsd, and a consumer
module is built against the package so a broken public surface fails
the build.

Releases need no registry upload: for Go the tag is the release. Since
the module lives in a subdirectory, the toolchain only sees tags
carrying that prefix, so publish.yml now pushes go/vX.Y.Z alongside the
release tag, and the version check covers all three ports.

The labeler tells the Go tests apart from the package itself, which
matters because Go keeps them in the same directory where python/ and
nodejs/ keep a sibling tests/ folder.
The per-package READMEs now describe only their own port, and the
project-wide pages list all three. The issue templates asked for a
"Python Version", which was already wrong for Node.js.
JeanExtreme002 and others added 4 commits August 24, 2026 20:19
The package readme now carries what the Python and Node.js ones do —
install, basic usage, documentation link — and nothing else. The deeper
material it held (entity constructors, client options, error handling,
TLS impersonation, the differences table) moves to docs/go.md, where the
equivalent Python and Node.js pages already live.
Each readme leads with its own workflow badge and then repeats the
project-wide block, so the Go one was missing Pypi, Npm, Downloads and
Frequency, and the other three were missing the Go reference and version.

The project-wide pages listed only some of the workflow badges: the root
readme had no Node.js one and the documentation home had neither Node.js
nor Go.
Each registry badge now sits next to the version it needs — Pypi with
Python, Npm with Node, the Go reference with Go — and the license moves
up beside the workflow badges.

The Node floor is the engines.node of nodejs/package.json.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved critical panics, timeout and retry handling, transport assumptions, and stale checksums remain, alongside moderate correctness issues.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a Go SDK alongside the existing Python and Node.js ports, with API parity, custom transport handling, tests, documentation, CI, and release support.

Changes:

  • Adds the Go module, client API, entities, parsers, cookies, retries, and decompression.
  • Adds generated data, parity tests, fixtures, examples, and development tooling.
  • Integrates Go into documentation, CI, dependency updates, and releases.

Final review findings:

  • go/flightradarapi/api.go:398,587Critical (1 vote): nil flight values can panic instead of returning the promised error.
  • go/flightradarapi/flight.go:445Moderate (2 votes): normalize missing or N/A aircraft images to an empty slice.
  • go/flightradarapi/flight.go:344Moderate (2 votes): validate all filter keys before evaluating criteria.
  • go/flightradarapi/parsers.go:225Moderate (3 votes): reject trailing non-whitespace after JSON.
  • go/flightradarapi/request.go:531Moderate (2 votes): wrap transport errors with ErrFlightRadar.
  • go/flightradarapi/request.go:491Critical (2 votes): negative timeouts must use the default timeout.
  • go/flightradarapi/request.go:224Critical (2 votes): prevent Jitter overflow and retry-duration panics.
  • go/flightradarapi/request.go:347Critical (1 vote): avoid assuming http.DefaultTransport has a concrete type.
  • go/go.sum:3Critical (4 votes): remove stale randomstring checksums.
File summaries
File Description
README.md Documents Go support and badges.
python/README.md Updates Python-specific documentation.
nodejs/README.md Updates Node.js-specific documentation.
mkdocs.yml Adds Go documentation navigation.
go/README.md Documents Go installation and usage.
go/Makefile Adds Go development, CI, and release targets.
go/LICENSE Adds Go module licensing.
go/go.sum Records dependency checksums.
go/go.mod Defines the Go module and dependencies.
go/flightradarapi/zones.go Provides generated zone data.
go/flightradarapi/values.go Provides payload conversion helpers.
go/flightradarapi/testdata/airports.json Supplies airport parser fixtures.
go/flightradarapi/testdata/airlines.html Supplies airline parser fixtures.
go/flightradarapi/snapshots_test.go Tests live response snapshots.
go/flightradarapi/request.go Implements HTTP, retries, decoding, and response limits.
go/flightradarapi/ports_test.go Enforces parity with Python sources.
go/flightradarapi/parsers.go Parses API payloads.
go/flightradarapi/parsers_test.go Tests parser behavior.
go/flightradarapi/flighttrackerconfig.go Implements tracker configuration.
go/flightradarapi/flight.go Implements flight entities and detail handling.
go/flightradarapi/example_test.go Provides usage examples.
go/flightradarapi/errors.go Defines SDK errors.
go/flightradarapi/entity.go Implements positioned entities and distance logic.
go/flightradarapi/entities_test.go Tests entity and filtering behavior.
go/flightradarapi/doc.go Documents the package and API mapping.
go/flightradarapi/countries.go Provides generated country constants.
go/flightradarapi/core.go Defines endpoints and request headers.
go/flightradarapi/cookies.go Implements scoped cookie handling.
go/flightradarapi/cookies_test.go Tests cookie behavior.
go/flightradarapi/airport.go Implements airport entities and parsing.
docs/index.md Adds the Go SDK documentation overview.
docs/go.md Documents Go API usage.
CONTRIBUTING.md Adds Go contribution guidance.
.gitignore Ignores Go artifacts and coverage output.
.github/workflows/publish.yml Adds Go release tagging and version validation.
.github/workflows/labeler.yml Adds Go-aware labels.
.github/workflows/go-package.yml Adds Go build, test, lint, and integration CI.
.github/ISSUE_TEMPLATE/questioning.md Updates question template options.
.github/ISSUE_TEMPLATE/bug_report.md Updates bug-report template options.
.github/dependabot.yml Adds Go dependency updates.
Review details

Suppressed comments (9)

go/flightradarapi/api.go:494

  • Ranging over a Go map does not preserve the feed's JSON object order, so GetFlights can return a different flight order on each call. Python and Node.js preserve the response order, and callers commonly rely on the returned list being stable; decode the feed with an order-preserving representation (and add an order assertion) before constructing this slice.
	for flightID, info := range content {
		if flightID == "" || flightID[0] < '0' || flightID[0] > '9' {
			continue

go/flightradarapi/api.go:763

  • request rejects every non-2xx status before returning a Response, so a server-side login failure (401/403/500, etc.) exits as *StatusError at line 765 and never reaches the success check. The Python and Node.js ports classify both non-2xx and success:false responses as LoginError; allow/classify the login response before generic status handling so errors.Is(err, ErrLogin) is consistent.
	response, err := c.client.request(ctx, c.endpoints.userLogin, requestOptions{
		headers: jsonHeaders,
		data:    data,
		timeout: c.Timeout,
	})

go/flightradarapi/api.go:74

  • newAPIClient installs bankRedirectCookies when HTTPClient.CheckRedirect is nil (request.go:426-428), so leaving this field unset is precisely how redirect cookies are banked. This comment says the opposite and may lead callers to provide a custom handler that silently disables cookie banking; correct the guidance.
	// size budget stays enforceable. Leave CheckRedirect unset, or the cookies
	// FR24 hands out on a redirect hop are not banked. Any Jar is ignored: this
	// package renders the Cookie header itself.

go/flightradarapi/api.go:587

  • For a logged-in caller, a nil flight reaches this dereference and panics instead of returning an error. Validate the pointer before constructing the historical-data URL, as this public method otherwise exposes a crash path for a representable input.
	response, err := c.client.request(ctx, c.endpoints.historicalDataURL(flight.ID, fileType, timestamp),

go/flightradarapi/request.go:405

  • json.Unmarshal maps JSON null to a nil map without an error, so this method returns (nil, nil) even though it promises to parse a JSON object. Callers that only check err can therefore treat a null response as a successful empty payload; reject content == nil before returning.
    go/flightradarapi/request.go:511
  • Although URL parsing errors are wrapped above, an error from http.NewRequestWithContext is returned raw here. This violates the package's documented guarantee that every error wraps ErrFlightRadar (doc.go:22) and makes errors.Is(err, ErrFlightRadar) fail for request-construction failures; wrap the underlying error.
    go/flightradarapi/request.go:551
  • A response-body read failure is also returned raw, so callers lose the package-wide ErrFlightRadar classification for broken connections or failing readers. Wrap this error with the response URL and underlying cause.
    go/flightradarapi/request.go:561
  • Errors from decodeBody (for example, malformed gzip/deflate/brotli data) escape without wrapping ErrFlightRadar, contrary to the package and documentation contract. Wrap the decoder error while preserving any existing sentinel such as ErrDecompressionLimit.
    go/flightradarapi/request.go:441
  • These request entry points return runWithRetry errors verbatim. Transport/read failures from c.httpClient.Do and io.ReadAll, as well as cancellation during retry backoff, can therefore escape as raw *url.Error, I/O, or context errors even though the package documentation promises that every error wraps ErrFlightRadar; wrap non-package errors at this boundary while preserving the underlying error for errors.Is/errors.As in both entry points.
  • Files reviewed: 41/43 changed files
  • Comments generated: 8
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread go/flightradarapi/api.go
Comment thread go/flightradarapi/flight.go Outdated
Comment thread go/flightradarapi/flight.go
Comment thread go/flightradarapi/parsers.go
Comment thread go/flightradarapi/request.go Outdated
Comment thread go/flightradarapi/request.go Outdated
Comment thread go/flightradarapi/request.go Outdated
Comment thread go/flightradarapi/request.go Outdated
Eight findings from the review on the pull request, each reproduced
before it was changed:

- CheckInfo reported an unknown field only when Go's randomised map
  iteration put it before a criterion that fails: 26 of 200 runs on the
  same input. Every criterion is now validated before any is evaluated.
- GetFlightDetails and GetHistoryData take a pointer, so nil is valid at
  compile time and panicked instead of returning an error.
- A body with data spliced after the JSON was accepted, where the Python
  and Node.js parsers reject it: Decode reads the first value only.
- Network failures did not wrap ErrFlightRadar, contradicting the
  taxonomy documented on that sentinel. They now wrap both it and the
  transport's own cause, which the retry policy still reads.
- A negative Client.Timeout skipped the deadline entirely instead of
  falling back to the default the field documents.
- New() panicked when the process had replaced http.DefaultTransport,
  which mocking libraries legitimately do.
- SleepFor panicked on the largest Jitter a Duration can hold, and could
  wrap round when adding it to the delay.
- Missing aircraft images stayed nil where the other ports default to an
  empty list, which changes both a CheckInfo comparison and the JSON.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Critical issues remain in cookie path matching, retry validation, the request-test assertion, and stale go.sum checksums; moderate issues remain in feed ordering and cancellation error wrapping.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (7)

Previously missed (2) — in code that hasn't changed since the last review.

go/flightradarapi/api.go:503

  • content was decoded into a Go map, whose iteration order is deliberately randomized. As a result, identical feed responses can produce flights in a different order on successive calls, unlike the ordered Python/Node.js results; preserve the feed's object order (or otherwise define a stable ordering) before building this slice.
	for flightID, info := range content {
		if flightID == "" || flightID[0] < '0' || flightID[0] > '9' {
			continue
		}
		if row, ok := info.([]any); ok {
			flights = append(flights, newFlight(flightID, row))
		}
	}

go/flightradarapi/request.go:287

  • When cancellation happens during the retry backoff, this returns ctx.Err() directly. That violates the package's documented guarantee that every returned error wraps ErrFlightRadar, so callers cannot consistently classify this failure with errors.Is(err, ErrFlightRadar) even though transport cancellations are wrapped.

.github/workflows/go-package.yml:13

  • ports_test.go reads the Python sources to enforce parity, but neither trigger includes python/**. A Python change can therefore alter the source of truth without running this workflow, allowing generated Go data or API drift to go undetected; include the Python tree in both path filters.
    paths:
      - 'go/**'
      - '.github/workflows/go-package.yml'
  pull_request:
    paths:
      - 'go/**'
      - '.github/workflows/go-package.yml'

go/flightradarapi/api.go:774

  • The request layer turns every non-2xx response into a StatusError before this method can inspect the login payload, so ordinary authentication failures such as HTTP 400/401 cannot become the documented LoginError and callers cannot match ErrLogin. Allow the login response to be parsed for failure statuses, while still requiring both a 2xx status and success for a successful login.
	response, err := c.client.request(ctx, c.endpoints.userLogin, requestOptions{
		headers: jsonHeaders,
		data:    data,
		timeout: c.Timeout,
	})

	if err != nil {
		return err

go/flightradarapi/request.go:684

  • Header.Get returns only the first value when Content-Encoding is sent in multiple header fields. That is equivalent to a comma-separated encoding list on the wire, but this loop then decodes only one layer and can leave the body compressed, despite the stated support for stacked encodings. Join Header.Values("Content-Encoding") before splitting.
    go/flightradarapi/request.go:428
  • Unmarshalling JSON null into a map succeeds and leaves content == nil, so Response.JSON returns (nil, nil) even though its contract is to parse a JSON object; plain endpoints can therefore report success with no object. Reject a nil map after unmarshalling with an ErrFlightRadar-wrapped error.
    go/flightradarapi/request.go:413
  • HTTP media types are case-insensitive, but this exact-case check rejects a valid Content-Type: Application/JSON response before parsing it. Normalize or parse the media type case-insensitively so JSON endpoints do not fail based on header casing.
  • Files reviewed: 41/43 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread go/flightradarapi/cookies.go Outdated
Comment thread go/flightradarapi/request.go Outdated
Comment thread go/flightradarapi/request_test.go
A connection dropped partway through a response was classified as
permanent, because the body is read after Do returns and so carries no
*url.Error of its own: against a server that promises 5000 bytes and
hangs up after 6, a policy asking for three attempts made one. The
Python port retries the same failure, where curl_cffi reads the body
inside the call. The read failure is now wrapped the way the transport
wraps its own.

Two readers were narrower than they read: nativeNumber covered every
numeric kind except float64, so a defined type over float64 was refused
where one over int64 worked, and getString ignored json.Number, which
this package itself produces when it decodes with UseNumber.
The jar collapsed same-named cookies to one, so a request to /data/...
carried the root token where FR24 had scoped a different value to that
path. RFC 6265 5.4 asks for every match, longest path first and oldest
first among equal paths, which is also what the cookie jar behind the
Python port does. get() still answers with the newest re-issue: it says
which token is current, not what to put on the wire.

A negative BaseDelay reached the overflow guard added with the last
round of fixes and came back as the longest sleep a Duration can hold —
292 years before the first retry. The zero BaseDelay that turns into NaN
once the doubling overflows landed in the same place.
GetBounds was asserted twice with the same call and different constants,
and the "box surrounds the point" test was subsumed by the one comparing
all four values against the numbers the Python suite pins.

Its two assertions moved into the survivor rather than being dropped:
they are what catches an expected-value fixture updated the wrong way.
Reordering the fields and "fixing" the fixture to match leaves the exact
comparison passing and fails on the shape.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Unresolved dependency, error-wrapping, input-validation, and documentation issues remain.

Review details

Suppressed comments (4)

Previously missed (4) — in code that hasn't changed since the last review.

go/flightradarapi/request.go:292

  • When a request is canceled while waiting between retry attempts, this returns context.Canceled/context.DeadlineExceeded directly, so errors.Is(err, ErrFlightRadar) is false even though the package documentation says every returned error wraps ErrFlightRadar. Wrap the context error with the package sentinel here, consistent with the other request failure paths.
    go/flightradarapi/api.go:224
  • The documentation defines only zero as the default, but this also silently converts any negative flightLimit or page into a valid request for the default/first page. That hides invalid caller input and diverges from the Python and Node.js methods, which forward an explicitly supplied negative value and let the API reject it; preserve negative inputs (or return a validation error) and default only on zero.
	if flightLimit <= 0 {
		flightLimit = defaultFlightLimit
	}
	if page <= 0 {
		page = 1

go/flightradarapi/api.go:655

  • As with GetAirportDetails, the Go docs say zero selects the default, but a negative limit is silently changed to 50 instead of being preserved/rejected as an explicit invalid argument. This masks caller errors and does not match the sibling SDKs; default only when limit == 0 (or return a validation error for negatives).
	if limit <= 0 {
		limit = defaultSearchLimit

docs/go.md:25

  • This setup example uses time.Second, but its only shown import is the SDK package, so copying the block produces an undefined time compile error. Add the standard-library import (or omit the options example) so the documented basic usage is self-contained.
```go
import "github.com/JeanExtreme002/FlightRadarAPI/go/flightradarapi"

client := flightradarapi.New()

// Every field's zero value means the default, so set only what you need.
client = flightradarapi.New(flightradarapi.Options{
    Timeout:    10 * time.Second,  // default: 30s
  • Files reviewed: 41/43 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

GetFlights walked a Go map, whose iteration order is randomised, so the
same feed answered in a different order on every call where the Python
and Node.js ports keep the order FR24 sent. The keys are now read from
the body itself.

Four smaller ones, each reproduced first:

- a body of "null" unmarshalled into a nil map with no error, so every
  key read as missing instead of the caller seeing the failure
- Content-Type was matched case-sensitively, though a media type is not
- Content-Encoding split across header fields decoded only its first
  layer and left the body compressed
- cancellation during the retry backoff returned the bare context error,
  outside the taxonomy every other failure path follows

A negative flightLimit, page or limit is no longer rewritten as the
default: only zero selects it, and anything else goes to FR24 as given,
which is what the sibling ports do.

The workflow now also runs when python/FlightRadarAPI changes, since
ports_test.go reads those files to detect drift and could not see a
change that never triggered it.
Copilot stopped reviewing on behalf of JeanExtreme002 due to an error August 25, 2026 01:49
SetFlightTrackerConfig copied the struct it was given without looking at
it. Go's zero value is the empty string where the Python dataclass
carries a default, so a literal {Limit: "10"} sent the feed
adsb=&air=&estimated=&faa=… — while the very same empty value was
rejected when it arrived through the values map. Unset fields now take
the default and every field is validated, so the two paths agree.

RetryPolicy read a zero MaxDelay as "no cap at all", which let a struct
literal climb to a four-minute sleep where the Python constructor caps
at thirty seconds. Zero now means the default the other ports declare,
for BaseDelay as well, so &RetryPolicy{MaxAttempts: 5} backs off exactly
like NewRetryPolicy(5). A zero Jitter still means none: a deterministic
test wants to be able to ask for that.

A cookie with Domain=localhost sent from localhost was discarded by the
guard against a bare TLD, though RFC 6265 5.3.5 allows a domain that is
the host itself.
The header was written out while its comment claimed it came from the
decoder table, so dropping a decoder would have left the client asking
for an encoding it can no longer read — and the body would come back
compressed, parsed as garbage rather than failing. It is now built from
the table, with the order kept apart as the one part that is a choice.

staticcheck and govulncheck ran as @latest inside a required CI step,
the only tools in these workflows not pinned, so an upstream release
could turn the build red on a tree nobody touched. Both are pinned in
the Makefile, which the workflow now calls instead of repeating the
command.

Two comments had drifted from their code: the one on matching still
described the one-cookie-per-name behaviour removed in 56c4fdf, and
GetAirlineLogo carried an unreachable 5xx branch that read as though a
server error fell through to the alternative URL.
@JeanExtreme002
JeanExtreme002 merged commit 81807f2 into main Aug 25, 2026
11 checks passed
@github-actions
github-actions Bot deleted the feat/go-sdk branch August 25, 2026 02:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants