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
@@ -261,7 +283,7 @@ function fromGaxiosError(errorObj: object): ErrorShape | undefined {
261283 if ( typeof data === 'string' ) {
262284 try {
263285 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
264- data = JSON . parse ( data ) ;
286+ data = JSON . parse ( sanitizeJsonString ( data ) ) ;
265287 } catch ( _ ) {
266288 // Not a JSON string, can't parse.
267289 }
@@ -311,7 +333,7 @@ function fromApiError(errorObj: object): ErrorShape | undefined {
311333 if ( typeof data === 'string' ) {
312334 try {
313335 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
314- data = JSON . parse ( data ) ;
336+ data = JSON . parse ( sanitizeJsonString ( data ) ) ;
315337 } catch ( _ ) {
316338 // Not a JSON string, can't parse.
317339 // Try one more fallback: look for the first '{' and last '}'
@@ -321,7 +343,9 @@ function fromApiError(errorObj: object): ErrorShape | undefined {
321343 if ( firstBrace !== - 1 && lastBrace !== - 1 && lastBrace > firstBrace ) {
322344 try {
323345 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
324- data = JSON . parse ( data . substring ( firstBrace , lastBrace + 1 ) ) ;
346+ data = JSON . parse (
347+ sanitizeJsonString ( data . substring ( firstBrace , lastBrace + 1 ) ) ,
348+ ) ;
325349 } catch ( __ ) {
326350 // Still failed
327351 }
0 commit comments