Skip to content

perf(proxy): trim routes from stops/location response - #167

Draft
ai-tiro wants to merge 1 commit into
fix/162-stopdetail-foreground-dupfrom
fix/163-backend-stops-payload
Draft

perf(proxy): trim routes from stops/location response#167
ai-tiro wants to merge 1 commit into
fix/162-stopdetail-foreground-dupfrom
fix/163-backend-stops-payload

Conversation

@ai-tiro

@ai-tiro ai-tiro commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Closes #163

Stacked on #166 (fix/162-stopdetail-foreground-dup). Base is the stacked branch, not master. The change is pure Go (backend/) and independent of the mobile edits below it in the stack — no conflict.

What

The /api/v3/stops/location response used by the Nearby map carried a full per-stop routes array. The proxy now strips that array from the response for that endpoint only, before returning it to the client. All other endpoints keep their existing verbatim io.Copy passthrough.

trimStopsLocation decodes the envelope and the stops array as raw JSON messages, deletes only the routes key from each stop object, and leaves every other field — top-level (disruptions, status) and per-stop (stop_id, stop_name, stop_latitude, stop_longitude, route_type, stop_suburb, …) — byte-for-byte intact. On any structural surprise (not an object, stops not an array, a non-object stop, marshal error) it returns the body unchanged and ServeHTTP falls back to a verbatim copy, so the endpoint can never be broken by an unexpected upstream shape.

Measured size win

Real upstream body, Melbourne CBD (stops/location/-37.8136,144.9631?max_results=100&max_distance=500):

bytes
before (with routes) 38978
after (routes stripped) 7535
reduction 80.7%

Verified by feeding the captured live response through trimStopsLocation directly. This matches the 38–41 KB figure in the issue and removes the field that dominates bytes-on-wire (departures are ~120 B–2 KB).

What I confirmed about client field usage (the critical safety check)

The mobile client does not consume routes from this endpoint:

  • The nearby response deserialises into StopDto (mobile/core/network/.../model/StopDto.kt), which declares only stop_id, stop_name, stop_suburb, route_type, stop_latitude, stop_longitudethere is no routes field on the DTO at all. kotlinx.serialization is configured with ignoreUnknownKeys, so routes was already being silently dropped on-device.
  • The mapper StopDto.toDomain() reads only those six fields.
  • The Nearby pins render from those fields only. The bottom sheet's route list is fetched separately via the per-stop detail endpoint (stopDetailRepository.getStopDetail(...)), not from this response.

So trimming routes here is invisible to every current consumer.

What I tried that didn't work / alternatives considered

  • Ask PTV to omit routes upstream (cheapest, no transform). Checked docs/ptv-timetable-api-v3-swagger.json: the stops/location op only exposes route_types, max_results, max_distance, stop_disruptions — no expand/include/exclude lever. PTV always returns routes. So a server-side strip is the only option.
  • Side note while measuring: passing route_types=0,1,2,3 (comma-joined) to the live proxy returns an empty stops:[]; PTV wants repeated route_types= params. Not in scope here, just noting it tripped up my first measurement.

Testing

  • cd backend && go build ./... && go vet ./... && go test ./... — all pass.
  • New handler tests:
    • TestHandler_StopsLocationTrimsRoutes — asserts routes/geopath/route_gtfs_id are gone, all pin fields + disruptions/status survive, body is valid JSON and smaller.
    • TestHandler_NonStopsLocationPassthroughKeepsRoutes — a stops/{id}/route_type/{type}-style body that legitimately carries routes is returned byte-for-byte unchanged, proving the trim is scoped.
    • TestTrimStopsLocation_Fallbacks — malformed JSON, non-object top level, missing stops, non-array stops, non-object stop, and no-routes-present all return the body untouched.

Justification / caveats

  • Scoped strictly to /v3/stops/location/ and only on a 200 response; everything else is untouched.
  • The fallback-to-verbatim-on-any-surprise design means the worst case is "no size win", never "broken endpoint".
  • No one-way-door changes. If a future client wants per-pin routes, it can use the detail endpoint (as the bottom sheet already does) or this trim can be removed.

🤖 Generated with Claude Code

The /api/v3/stops/location response used by the Nearby map carried a
full `routes` array on every stop (each route with route_name,
route_gtfs_id, route_number and a geopath:[] placeholder). For a typical
Melbourne CBD fetch (max_results=100, max_distance=500) this is the
dominant bytes-on-wire cost: ~39 KB, of which the pins use none of it.

The mobile client deserialises the nearby response into StopDto, which
declares only stop_id/stop_name/stop_suburb/route_type/stop_latitude/
stop_longitude and has no `routes` field at all — kotlinx.serialization
silently drops it via ignoreUnknownKeys. The Nearby bottom sheet fetches
routes separately via the per-stop detail endpoint, not from this
response. So `routes` here is pure dead weight on the wire.

PTV's v3 API has no query param to omit routes (the stops/location op
only exposes route_types/max_results/max_distance/stop_disruptions), so
the only lever is to strip it in the proxy. trimStopsLocation decodes the
envelope and stops array as raw messages, deletes only the `routes` key
from each stop, and leaves every other field byte-for-byte intact. On any
structural surprise it returns the body unchanged and ServeHTTP falls
back to a verbatim copy, so the endpoint can never be broken by an
unexpected upstream shape. The trim is scoped to /v3/stops/location/ only;
all other endpoints keep their existing io.Copy passthrough.

Measured on the real upstream body: 38978 -> 7535 bytes (80.7% smaller).

Co-Authored-By: ai-tiro <ai-tiro@jfx.ac>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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