99 * This file contains types and functions for parsing structured Google API errors.
1010 */
1111
12+ /**
13+ * Sanitize a JSON string before parsing to handle known SSE stream corruption.
14+ * SSE stream parsing can inject stray commas — the observed pattern is a comma
15+ * at the end of one line followed by a stray comma on the next line, e.g.:
16+ * `"domain": "cloudcode-pa.googleapis.com",\n , "metadata": {`
17+ * This collapses duplicate commas (possibly separated by whitespace/newlines)
18+ * into a single comma, preserving the whitespace.
19+ */
20+ function sanitizeJsonString ( jsonStr : string ) : string {
21+ // Match a comma, optional whitespace/newlines, then another comma.
22+ // Replace with just a comma + the captured whitespace.
23+ // Loop to handle cases like `,,,` which would otherwise become `,,` on a single pass.
24+ let prev : string ;
25+ do {
26+ prev = jsonStr ;
27+ jsonStr = jsonStr . replace ( / , ( \s * ) , / g, ',$1' ) ;
28+ } while ( jsonStr !== prev ) ;
29+ return jsonStr ;
30+ }
31+
1232/**
1333 * Based on google/rpc/error_details.proto
1434 */
@@ -138,7 +158,7 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null {
138158 // If error is a string, try to parse it.
139159 if ( typeof errorObj === 'string' ) {
140160 try {
141- errorObj = JSON . parse ( errorObj ) ;
161+ errorObj = JSON . parse ( sanitizeJsonString ( errorObj ) ) ;
142162 } catch ( _ ) {
143163 // Not a JSON string, can't parse.
144164 return null ;
@@ -168,7 +188,9 @@ export function parseGoogleApiError(error: unknown): GoogleApiError | null {
168188 try {
169189 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
170190 const parsedMessage = JSON . parse (
171- currentError . message . replace ( / \u00A0 / g, '' ) . replace ( / \n / g, ' ' ) ,
191+ sanitizeJsonString (
192+ currentError . message . replace ( / \u00A0 / g, '' ) . replace ( / \n / g, ' ' ) ,
193+ ) ,
172194 ) ;
173195 if ( parsedMessage . error ) {
174196 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
@@ -260,7 +282,7 @@ function fromGaxiosError(errorObj: object): ErrorShape | undefined {
260282 if ( typeof data === 'string' ) {
261283 try {
262284 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
263- data = JSON . parse ( data ) ;
285+ data = JSON . parse ( sanitizeJsonString ( data ) ) ;
264286 } catch ( _ ) {
265287 // Not a JSON string, can't parse.
266288 }
@@ -310,7 +332,7 @@ function fromApiError(errorObj: object): ErrorShape | undefined {
310332 if ( typeof data === 'string' ) {
311333 try {
312334 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
313- data = JSON . parse ( data ) ;
335+ data = JSON . parse ( sanitizeJsonString ( data ) ) ;
314336 } catch ( _ ) {
315337 // Not a JSON string, can't parse.
316338 // Try one more fallback: look for the first '{' and last '}'
@@ -320,7 +342,9 @@ function fromApiError(errorObj: object): ErrorShape | undefined {
320342 if ( firstBrace !== - 1 && lastBrace !== - 1 && lastBrace > firstBrace ) {
321343 try {
322344 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
323- data = JSON . parse ( data . substring ( firstBrace , lastBrace + 1 ) ) ;
345+ data = JSON . parse (
346+ sanitizeJsonString ( data . substring ( firstBrace , lastBrace + 1 ) ) ,
347+ ) ;
324348 } catch ( __ ) {
325349 // Still failed
326350 }
0 commit comments