Environment
- google-ads-api: 24.1.0
- Node: v24.18.0
- Transport:
customer.query(gaql) (non-streaming search)
What happened
When the API returns a REST-style structured error (a google.rpc.ErrorInfo
with reason/domain/metadata as plain fields -- in our case
reason: "SERVICE_DISABLED", because the Google Ads API wasn't yet enabled
on our Cloud project), customer.query() throws:
TypeError: Cannot read properties of undefined (reading 'get')
at Customer.getGoogleAdsError (node_modules/google-ads-api/build/src/service.js:112:49)
at Customer.querier (node_modules/google-ads-api/build/src/customer.js:286:41)
at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
at async Customer.query (node_modules/google-ads-api/build/src/customer.js:26:30)
This completely destroys the original error -- callers never see
SERVICE_DISABLED, the activation_url to fix it, or anything else useful,
only the crash.
Root cause
service.js:
getGoogleAdsError(error) {
if (typeof error?.metadata?.internalRepr.get(exports.FAILURE_KEY) === "undefined") {
return error;
}
const [buffer] = error.metadata.internalRepr.get(exports.FAILURE_KEY);
return this.decodeGoogleAdsFailureBuffer(buffer);
}
The optional chain (error?.metadata?.internalRepr) stops one property
short: it guards error and error.metadata being nullish, but then calls
.get(...) directly on internalRepr without ?.. For a genuine gRPC-
trailer-encoded GoogleAdsFailure, internalRepr is a Map and this
works. For a REST-style structured error (no gRPC trailers at all --
error.metadata is a plain object like { service, consumer, container_info, service_title, activation_url } with no internalRepr
key), error.metadata.internalRepr is undefined, and undefined.get(...)
throws.
Reproduction
Any account/permission/enablement error that Google returns as a
google.rpc.ErrorInfo-shaped body reproduces this -- confirmed with
SERVICE_DISABLED when the Google Ads API wasn't yet enabled on the
calling Cloud project, but the same crash would occur for any other reason
value returned in this shape (e.g. PERMISSION_DENIED,
API_KEY_INVALID, CONSUMER_INVALID).
const { GoogleAdsApi } = require("google-ads-api");
const client = new GoogleAdsApi({ client_id, client_secret, developer_token });
const customer = client.Customer({ customer_id, login_customer_id, refresh_token });
// If the Google Ads API is not enabled on the Cloud project behind
// client_id/client_secret, this throws the opaque TypeError above
// instead of surfacing SERVICE_DISABLED.
await customer.query(`SELECT campaign.id FROM campaign LIMIT 1`);
Suggested fix
Guard the missing ?. before .get(...):
getGoogleAdsError(error) {
const internalRepr = error?.metadata?.internalRepr;
if (!internalRepr || typeof internalRepr.get !== "function") {
return error;
}
const entry = internalRepr.get(exports.FAILURE_KEY);
if (typeof entry === "undefined") {
return error;
}
const [buffer] = entry;
return this.decodeGoogleAdsFailureBuffer(buffer);
}
This returns the original (REST-style) error object as-is when it isn't a
gRPC-trailer-shaped failure, instead of crashing -- callers can then inspect
error.reason / error.domain / error.metadata themselves.
Happy to open a PR with this change if useful.
Environment
customer.query(gaql)(non-streaming search)What happened
When the API returns a REST-style structured error (a
google.rpc.ErrorInfowith
reason/domain/metadataas plain fields -- in our casereason: "SERVICE_DISABLED", because the Google Ads API wasn't yet enabledon our Cloud project),
customer.query()throws:This completely destroys the original error -- callers never see
SERVICE_DISABLED, theactivation_urlto fix it, or anything else useful,only the crash.
Root cause
service.js:The optional chain (
error?.metadata?.internalRepr) stops one propertyshort: it guards
erroranderror.metadatabeing nullish, but then calls.get(...)directly oninternalReprwithout?.. For a genuine gRPC-trailer-encoded
GoogleAdsFailure,internalRepris aMapand thisworks. For a REST-style structured error (no gRPC trailers at all --
error.metadatais a plain object like{ service, consumer, container_info, service_title, activation_url }with nointernalReprkey),
error.metadata.internalReprisundefined, andundefined.get(...)throws.
Reproduction
Any account/permission/enablement error that Google returns as a
google.rpc.ErrorInfo-shaped body reproduces this -- confirmed withSERVICE_DISABLEDwhen the Google Ads API wasn't yet enabled on thecalling Cloud project, but the same crash would occur for any other reason
value returned in this shape (e.g.
PERMISSION_DENIED,API_KEY_INVALID,CONSUMER_INVALID).Suggested fix
Guard the missing
?.before.get(...):This returns the original (REST-style) error object as-is when it isn't a
gRPC-trailer-shaped failure, instead of crashing -- callers can then inspect
error.reason/error.domain/error.metadatathemselves.Happy to open a PR with this change if useful.