Skip to content

getGoogleAdsError crashes decoding a REST-style structured error (e.g. SERVICE_DISABLED), masking the real error #548

Description

@CentralPointPharmacy

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions