Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions handwritten/bigtable/src/utils/mutateInternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,13 +146,31 @@ export function mutateInternal(
a status code, the RPC level failure error code will be used as the
entry failure code.
*/
(err as ServiceError & {errors?: ServiceError[]}).errors =
const rpcError = err;
(rpcError as ServiceError & {errors?: ServiceError[]}).errors =
mutationErrors.concat(
[...pendingEntryIndices]
.filter(index => !mutationErrorsByEntryIndex.has(index))
.map(() => err),
.map((): ServiceError => {
// Carry the RPC-level code and message on a fresh error rather
// than pushing `rpcError` into its own `errors` array. A
// self-reference makes the returned error impossible to
// serialize for tools that follow the AggregateError convention
// of recursing into `errors` (e.g. pino).
const entryError = new Error(rpcError.message) as ServiceError;
entryError.code = rpcError.code;
// Preserve the gRPC debugging context that the former
// self-reference carried for each entry.
if (rpcError.metadata) {
entryError.metadata = rpcError.metadata;
}
if (rpcError.details) {
entryError.details = rpcError.details;
}
return entryError;
Comment thread
kyungseopk1m marked this conversation as resolved.
}),
);
collectMetricsCallback(err, err);
collectMetricsCallback(rpcError, rpcError);
return;
}
collectMetricsCallback(null, null);
Expand Down
17 changes: 16 additions & 1 deletion handwritten/bigtable/test/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import * as proxyquire from 'proxyquire';
import * as pumpify from 'pumpify';
import * as sinon from 'sinon';
import {PassThrough, Writable, Duplex} from 'stream';
import {ServiceError} from 'google-gax';
import {grpc, ServiceError} from 'google-gax';

import * as inst from '../src/instance';
import {ChunkTransformer} from '../src/chunktransformer.js';
Expand Down Expand Up @@ -2861,6 +2861,9 @@ describe('Bigtable/Table', () => {
];
const unretriableError = new Error('not retryable') as ServiceError;
unretriableError.code = 3; // INVALID_ARGUMENT
unretriableError.metadata = new grpc.Metadata();
unretriableError.metadata.set('x-goog-error', 'context');
unretriableError.details = 'invalid argument details';
emitters = [
((stream: Writable) => {
stream.emit('data', {
Expand Down Expand Up @@ -2889,6 +2892,18 @@ describe('Bigtable/Table', () => {
assert.strictEqual(err.errors.length, 1);
assert.strictEqual(err.errors[0].code, 3);
assert.strictEqual(err.errors[0].message, 'not retryable');
// Regression test for #8075: the per-entry error must not be
// `err` itself, otherwise `err.errors` references `err` and the
// error can no longer be serialized (e.g. by pino).
assert.notStrictEqual(err.errors[0], err);
assert.doesNotThrow(() => JSON.stringify(err));
// The per-entry error keeps the gRPC metadata and details from
// the RPC-level failure so debugging context isn't lost.
assert.strictEqual(err.errors[0].metadata, err.metadata);
assert.strictEqual(
err.errors[0].details,
'invalid argument details',
);
done();
} catch (e) {
done(e);
Expand Down
Loading