Skip to content

Commit 30d2499

Browse files
Fix PGCR route decode for plain JSON storage (API-1N). (#149)
Services stores uncompressed JSON in pgcr; always gunzipSync caused incorrect header check errors on /pgcr/:instanceId. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e2d647c commit 30d2499

3 files changed

Lines changed: 31 additions & 44 deletions

File tree

src/routes/pgcr.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,9 @@ import {
77
import { ErrorCode } from "@/schema/errors/ErrorCode"
88
import { zBigIntString } from "@/schema/input"
99
import { zInt64 } from "@/schema/output"
10-
import { getRawCompressedPGCR } from "@/services/pgcr"
11-
import { gunzipSync } from "bun"
10+
import { decodePgcrPayload, getRawCompressedPGCR } from "@/services/pgcr"
1211
import { z } from "zod"
1312

14-
const decoder = new TextDecoder()
15-
1613
export const pgcrRoute = new RaidHubRoute({
1714
method: "get",
1815
description: `Get a raw post game carnage report by instanceId.
@@ -46,8 +43,7 @@ Useful if you need to access PGCRs when Bungie's API is down.`,
4643
return RaidHubRoute.fail(ErrorCode.PGCRNotFoundError, { instanceId })
4744
}
4845

49-
const decompressed = gunzipSync(result.data)
50-
const pgcr = JSON.parse(decoder.decode(decompressed)) as RaidHubPostGameCarnageReport
46+
const pgcr = JSON.parse(decodePgcrPayload(result.data)) as RaidHubPostGameCarnageReport
5147
pgcr.activityDetails.instanceId = BigInt(pgcr.activityDetails.instanceId)
5248
pgcr.entries.forEach(entry => {
5349
entry.characterId = BigInt(entry.characterId)

src/services/pgcr.test.ts

Lines changed: 13 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,18 @@
1-
import { getFixturePool } from "@/lib/test-fixture-db"
2-
import { gzipPgcrJson } from "@/lib/test-minimal-pgcr"
3-
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
4-
import { z } from "zod"
5-
import { getRawCompressedPGCR } from "./pgcr"
1+
import { buildMinimalRaidHubPgcrJson, gzipPgcrJson } from "@/lib/test-minimal-pgcr"
2+
import { describe, expect, test } from "bun:test"
3+
import { decodePgcrPayload } from "./pgcr"
64

7-
const fixtureDb = getFixturePool()
8-
const fixturePgcrInstanceId = "999000000703"
5+
describe("decodePgcrPayload", () => {
6+
const instanceId = "999000000704"
97

10-
beforeAll(async () => {
11-
await fixtureDb.query(`DELETE FROM raw.pgcr WHERE instance_id = $1::bigint`, [
12-
fixturePgcrInstanceId
13-
])
14-
await fixtureDb.query(
15-
`INSERT INTO raw.pgcr (instance_id, data, date_crawled) VALUES ($1::bigint, $2, NOW())`,
16-
[fixturePgcrInstanceId, gzipPgcrJson(fixturePgcrInstanceId)]
17-
)
18-
})
19-
20-
afterAll(async () => {
21-
await fixtureDb.query(`DELETE FROM raw.pgcr WHERE instance_id = $1::bigint`, [
22-
fixturePgcrInstanceId
23-
])
24-
})
25-
26-
describe("getRawCompressedPGCR", () => {
27-
test("returns the correct shape", async () => {
28-
const data = await getRawCompressedPGCR(fixturePgcrInstanceId).catch(console.error)
8+
test("decodes gzip-compressed JSON", () => {
9+
const json = decodePgcrPayload(gzipPgcrJson(instanceId))
10+
expect(JSON.parse(json).activityDetails.instanceId).toBe(instanceId)
11+
})
2912

30-
const parsed = z
31-
.object({
32-
data: z.instanceof(Buffer)
33-
})
34-
.strict()
35-
.safeParse(data)
36-
if (!parsed.success) {
37-
console.error(parsed.error.errors)
38-
expect(parsed.error.errors).toEqual([])
39-
} else {
40-
expect(parsed.success).toBe(true)
41-
}
13+
test("decodes plain JSON", () => {
14+
const plain = Buffer.from(JSON.stringify(buildMinimalRaidHubPgcrJson(instanceId)))
15+
const json = decodePgcrPayload(plain)
16+
expect(JSON.parse(json).activityDetails.instanceId).toBe(instanceId)
4217
})
4318
})

src/services/pgcr.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
import { pgReader } from "@/integrations/postgres"
2+
import { gunzipSync } from "bun"
3+
4+
const decoder = new TextDecoder()
5+
6+
/** Gzip magic bytes — matches Services `log-raw-pgcr` and Hermes storage (plain JSON when absent). */
7+
function isGzipCompressed(data: Buffer): boolean {
8+
return data.length >= 2 && data[0] === 0x1f && data[1] === 0x8b
9+
}
10+
11+
/** Decode PGCR payload stored as gzip (legacy raw.pgcr) or plain JSON (pgcr table). */
12+
export function decodePgcrPayload(data: Buffer): string {
13+
if (isGzipCompressed(data)) {
14+
return decoder.decode(gunzipSync(data))
15+
}
16+
return decoder.decode(data)
17+
}
218

319
export async function getRawCompressedPGCR(instanceId: bigint | string) {
420
return await pgReader.queryRow<{

0 commit comments

Comments
 (0)