Files
orca/src/main/gitlab/glab-api-response.ts
T
2026-08-28 02:25:48 -07:00

101 lines
3.5 KiB
TypeScript

export type GlabApiResponse = {
body: string
headers: Record<string, string>
}
/** @internal - exported for tests through gl-utils. */
export function parseGlabApiResponse(stdout: string): GlabApiResponse {
// Why: response is HTTP status, headers, blank line, then body.
// Find the first blank line (CRLF or LF) as the boundary.
const separator = findHeaderBodySeparator(stdout)
if (!separator) {
return { body: stdout, headers: {} }
}
const headerBlock = stdout.slice(0, separator.index)
const body = stdout.slice(separator.bodyStart)
const headers: Record<string, string> = {}
// Skip the status line and parse the rest as key: value.
const lines = headerBlock.split(/\r?\n/)
for (const line of lines) {
const m = line.match(/^([A-Za-z][A-Za-z0-9-]*):\s*(.*)$/)
if (m) {
headers[m[1].toLowerCase()] = m[2].trim()
}
}
return { body, headers }
}
/** A GitLab pagination header, or undefined when absent, unparseable, or below `minimum`. */
export function parseGlabPaginationHeader(
value: string | undefined,
minimum: number
): number | undefined {
if (!value) {
return undefined
}
const parsed = Number.parseInt(value, 10)
return Number.isFinite(parsed) && parsed >= minimum ? parsed : undefined
}
/** A non-list body carrying no GitLab error text — opaque data, so there is nothing to classify. */
export class GlabNonListResponseError extends Error {}
// Why: glab allows a 10MB body; this is what keeps a proxy's whole response out of the error banner.
const REPORTED_PAYLOAD_LIMIT = 300
/**
* Parse a glab list response, failing readably when GitLab answers with a JSON object.
*
* Why: glab exits 0 on error envelopes and proxy wrappers, so `JSON.parse(...).map` threw an
* opaque `.map is not a function` that the caller's classifier could only report as "unknown".
*/
export function parseGlabJsonList<T>(payload: string): T[] {
const parsed: unknown = JSON.parse(payload)
if (Array.isArray(parsed)) {
return parsed as T[]
}
const reported = gitlabErrorText(parsed)
if (reported) {
throw new Error(`GitLab returned an error: ${reported}`)
}
// Why: slice the raw payload rather than re-serializing `parsed` — same text, without
// stringifying a multi-megabyte body just to keep the preview.
throw new GlabNonListResponseError(
`GitLab returned a non-list response: ${payload.trim().slice(0, REPORTED_PAYLOAD_LIMIT)}`
)
}
/** GitLab reports API failures as `{ message }` or `{ error }`; anything else is opaque data. */
function gitlabErrorText(parsed: unknown): string | null {
if (typeof parsed !== 'object' || parsed === null) {
return null
}
const { message, error } = parsed as { message?: unknown; error?: unknown }
for (const value of [message, error]) {
if (typeof value === 'string' && value.trim()) {
return value.trim().slice(0, REPORTED_PAYLOAD_LIMIT)
}
}
return null
}
function findHeaderBodySeparator(stdout: string): { index: number; bodyStart: number } | null {
let lineStart = 0
for (let index = 0; index < stdout.length; index++) {
const code = stdout.charCodeAt(index)
if (code !== 10 && code !== 13) {
continue
}
const lineEnd = index
const nextLineStart =
stdout.charCodeAt(index) === 13 && stdout.charCodeAt(index + 1) === 10 ? index + 2 : index + 1
if (lineEnd === lineStart) {
return { index: lineStart, bodyStart: nextLineStart }
}
lineStart = nextLineStart
index = nextLineStart - 1
}
return null
}