-
Notifications
You must be signed in to change notification settings - Fork 20
docs: document full response contract for POST /api/v1/locations #91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -154,38 +154,57 @@ The server updates its in-memory state with the latest position and persists the | |
|
|
||
| **`POST /api/v1/locations` validation and error contract** | ||
|
|
||
| The ingest endpoint performs strict request validation before writing data: | ||
| The ingest endpoint requires a driver JWT (`Authorization: Bearer <token>`) | ||
| and performs strict request validation before writing data: | ||
|
|
||
| - `Content-Type` must be `application/json` (charset parameters are allowed). | ||
| - The request body must contain exactly one JSON object. | ||
| - Unknown JSON fields are rejected. | ||
| - Standard payload validation still applies (`vehicle_id`, coordinates, timestamp). | ||
| - Reports are rate limited to one per five seconds per driver. | ||
|
|
||
| Response codes: | ||
|
|
||
| - `201 Created` — location accepted and persisted. | ||
| - `400 Bad Request` — invalid JSON or payload validation failure. | ||
| - `401 Unauthorized` — missing, malformed, or expired bearer token. | ||
| - `415 Unsupported Media Type` — non-JSON `Content-Type`. | ||
| - `429 Too Many Requests` — driver exceeded the ingest rate limit. | ||
|
Comment on lines
160
to
+172
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
rg -n -C 12 \
'handlePostLocation|VehicleRateLimiter|DisallowUnknownFields|StatusBadRequest|StatusUnsupportedMediaType|StatusTooManyRequests|json\.Decoder' \
--glob '*.go' .Repository: OneBusAway/vehicle-positions Length of output: 50385 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- matching files ---'
rg -l --glob '*.go' 'handlePostLocation|VehicleRateLimiter' .
printf '%s\n' '--- matching declarations and uses ---'
rg -n --glob '*.go' '^(type|func) .*?(VehicleRateLimiter|handlePostLocation)|VehicleRateLimiter|handlePostLocation' .Repository: OneBusAway/vehicle-positions Length of output: 3555 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
ast-grep outline handlers.go
ast-grep outline ratelimit.go
ast-grep outline handlers_test.goRepository: OneBusAway/vehicle-positions Length of output: 4901 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- handlers.go ---'
sed -n '1,140p' handlers.go
printf '%s\n' '--- ratelimit.go ---'
sed -n '1,95p' ratelimit.go
printf '%s\n' '--- README.md ---'
sed -n '150,180p' README.md
printf '%s\n' '--- validation and response tests ---'
sed -n '180,290p' handlers_test.go
sed -n '470,610p' handlers_test.go
printf '%s\n' '--- rate-limit and identity tests ---'
sed -n '606,755p' handlers_test.goRepository: OneBusAway/vehicle-positions Length of output: 24803 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- authentication and route wiring ---'
rg -n -C 18 'func authMiddleware|func requireAuth|StatusUnauthorized|Authorization|jwt.Parse|POST /api/v1/locations' --glob '*.go' .
printf '%s\n' '--- complete README contract and examples ---'
sed -n '160,215p' README.md
printf '%s\n' '--- static contract verifier ---'
python3 - <<'PY'
from pathlib import Path
handlers = Path("handlers.go").read_text()
ratelimit = Path("ratelimit.go").read_text()
checks = {
"JSON media type with parameters": 'mime.ParseMediaType(contentType)' in handlers,
"unknown fields rejected": 'decoder.DisallowUnknownFields()' in handlers,
"trailing JSON rejected": 'decoder.Decode(new(json.RawMessage))' in handlers,
"payload validation before save": 'if err := loc.validate(); err != nil' in handlers and
handlers.index('if err := loc.validate(); err != nil') < handlers.index('store.SaveLocation'),
"rate-limit key is JWT subject": 'rl.Allow(loc.DriverID)' in handlers and 'loc.DriverID = sub' in handlers,
"429 mapping": 'http.StatusTooManyRequests' in handlers,
"save precedes tracker update": handlers.index('store.SaveLocation') < handlers.index('tracker.Update'),
"capacity bypass exists": 'allowing untracked key' in ratelimit and
'return true' in ratelimit[ratelimit.index('if len(vrl.limiters) >= maxTrackedRates'):],
}
for name, result in checks.items():
print(f"{name}: {'PASS' if result else 'FAIL'}")
PYRepository: OneBusAway/vehicle-positions Length of output: 48530 Enforce the rate limit when the limiter reaches capacity.
🤖 Prompt for AI Agents |
||
| - `500 Internal Server Error` — the location could not be persisted. The | ||
| in-memory tracker is left untouched, so a failed write never reaches the | ||
| GTFS-RT feed. | ||
|
|
||
| Examples: | ||
|
|
||
| ```bash | ||
| # Valid request | ||
| curl -i -X POST http://localhost:8080/api/v1/locations \ | ||
| -H "Authorization: Bearer $TOKEN" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"vehicle_id":"bus-1","trip_id":"route-5","latitude":-1.29,"longitude":36.82,"timestamp":1752566400}' | ||
| -d '{"vehicle_id":"bus-1","trip_id":"route-5","latitude":-1.29,"longitude":36.82,"timestamp":'"$(date +%s)"'}' | ||
|
|
||
| # Missing bearer token -> 401 | ||
| curl -i -X POST http://localhost:8080/api/v1/locations \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"vehicle_id":"bus-1","latitude":-1.29,"longitude":36.82,"timestamp":'"$(date +%s)"'}' | ||
|
|
||
| # Invalid content type -> 415 | ||
| curl -i -X POST http://localhost:8080/api/v1/locations \ | ||
| -H "Authorization: Bearer $TOKEN" \ | ||
| -H "Content-Type: text/plain" \ | ||
| -d '{"vehicle_id":"bus-1","latitude":-1.29,"longitude":36.82,"timestamp":1752566400}' | ||
| -d '{"vehicle_id":"bus-1","latitude":-1.29,"longitude":36.82,"timestamp":'"$(date +%s)"'}' | ||
|
|
||
| # Trailing JSON value -> 400 | ||
| curl -i -X POST http://localhost:8080/api/v1/locations \ | ||
| -H "Authorization: Bearer $TOKEN" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"vehicle_id":"bus-1","latitude":-1.29,"longitude":36.82,"timestamp":1752566400}{"extra":1}' | ||
| -d '{"vehicle_id":"bus-1","latitude":-1.29,"longitude":36.82,"timestamp":'"$(date +%s)"'}{"extra":1}' | ||
| ``` | ||
|
|
||
| `$TOKEN` is the JWT returned by `POST /api/v1/auth/login`. The examples use | ||
| `$(date +%s)` rather than a fixed timestamp because reports more than five | ||
| minutes from server time are rejected with `400`. | ||
|
|
||
| **Technology Stack:** | ||
|
|
||
| - **Language:** Go (aligns with Maglev and OTSF’s server-side direction) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: OneBusAway/vehicle-positions
Length of output: 50385
🏁 Script executed:
Repository: OneBusAway/vehicle-positions
Length of output: 50384
🏁 Script executed:
Repository: OneBusAway/vehicle-positions
Length of output: 12869
Enforce the driver role or update the documentation.
POST /api/v1/locationsuses authentication only.handlePostLocationreadssubbut does not checkrole, so any valid JWT with a non-emptysubcan submit locations.🤖 Prompt for AI Agents