node-postgres: "empty password returned by client"
ENGINEERING
AUGUST 31, 2026

node-postgres: "empty password returned by client"

5 min read
BACK TO BLOG

The error names the symptom and not one component that caused it. Object.assign let a parsed connection string overwrite the function minting the IAM token.

If you landed here from a search, the short version is at the bottom under **The fix**. The rest is why it took a while to find, which is the part I think is worth reading. ## The setup A service connecting to RDS Postgres with IAM authentication rather than a stored password. The appeal is obvious: no long-lived database credential anywhere in the system, no secret to rotate, no secret to leak. The token is minted on demand and expires in fifteen minutes. In `node-postgres` you do that by giving the `Pool` a `password` that is a function rather than a string. The driver calls it for each new connection, and it returns a freshly signed token. ```js new Pool({ host, port, database, user, password: async () => signer.getAuthToken(), ssl: { rejectUnauthorized: true }, }) ``` That works. What did not work was the version I actually had, which built config from a connection string first and then layered the IAM bits on top. ## The error ``` error: empty password returned by client ``` That is the whole message. It arrives on every connection attempt, immediately, in a service that had been working the week before. Read it literally and it is accurate: the client returned an empty password. What it does not tell you is that a *function* was supposed to be there, that something replaced it, or where. It describes the last moment before failure and nothing about the cause. ## Why is this hard to find? I did what the message suggests, in roughly this order. **Is the token function throwing?** Logged inside it. It never ran. That is the first genuinely useful signal, and it should have redirected me faster than it did — a function that is never called has usually been replaced, not broken. **Is the IAM policy wrong?** Checked `rds-db:connect`, checked the resource ARN down to the database user. Fine. This was the expensive detour: an IAM problem and a config problem look identical from the application, and IAM is where you expect the trouble to be with a setup like this. **Is the token expiring?** No — it was never being minted. **Is it SSL?** RDS IAM auth requires TLS, and getting that wrong produces its own confusing errors. Not this one. Somewhere in there I stopped debugging the database and started reading how the config object was assembled, which is where it was the whole time. ## The cause The config was built in two steps. Parse the connection string for host, port, database and user, then merge in the IAM-specific pieces: ```js const parsed = parse(process.env.DATABASE_URL) // → { ..., password: '' } const config = Object.assign( { password: async () => signer.getAuthToken() }, parsed, // ← this wins ) ``` `Object.assign` copies left to right, and **later sources overwrite earlier ones**. The parsed connection string carries a `password` key. There is no password in the URL, so its value is an empty string — and an empty string is a perfectly valid value to assign. It silently replaced the function. No error, no warning, no type complaint. `password` had the right name and the wrong type, and nothing in the chain cared until Postgres was handed an empty string and said so. ## The fix Put the thing that must win last: ```js const config = { ...parse(process.env.DATABASE_URL), password: async () => signer.getAuthToken(), // last, so it survives } ``` And assert it, because the failure mode is silent and will recur the moment somebody adds another merge: ```js if (typeof config.password !== 'function') { throw new Error('IAM token function was overwritten during config assembly') } ``` That throw is worth more than the fix. The fix stops today's bug; the assertion turns tomorrow's version of it into an error message that names the actual problem. ## What I took from it **A credential that is a function is a different kind of thing from a credential that is a string,** and most config plumbing was written assuming strings. Spreads, merges, `Object.assign`, defaults-then-overrides — all of it will cheerfully swap a function for an empty string, because at the type level nothing is wrong. **When a callback never fires, suspect replacement before failure.** I lost the most time to IAM policy because that is where I expected a problem to be, not because the evidence pointed there. The evidence — a function that never ran — was pointing at assignment from the start. **Error messages describe the last moment, not the cause.** "Empty password returned by client" is a true statement about the final microsecond of a chain of events, and no part of it is about `Object.assign`. Worth remembering when the message and the bug seem to have nothing to do with each other: that is normal, not a sign you are missing something clever. This came out of a multi-tenant clinical platform I designed and built — 95 TypeScript source files, 29 test files, 36 migrations. It is built and owned by me, with no live client, so nobody's patients were affected by any of this. The architecture, the isolation tests and two other bugs of this shape are public: **[github.com/jaklabs/telehealth-platform-reference](https://github.com/jaklabs/telehealth-platform-reference)** If you want to judge whether I am worth listening to before reading any further, the **[free website check](/website-audit)** is a public unauthenticated endpoint that takes an arbitrary URL from a stranger and drives headless Chromium at it, with the SSRF boundary that implies. Poke at it rather than take my word for anything. And if you are shipping something where being wrong is expensive and you want another pair of eyes on the parts that fail quietly, that is what I do. [Tell me what you built](/contact).
SHARE
5 min read
Read More

MORE ARTICLES

Two ECS failures that produce no useful error
Engineering
Engineering

Two ECS failures that produce no useful error

A readiness probe blocked by the task's own IAM role, and readonlyRootFilesystem silently killing ECS Exec. Both look like broken infrastructure.

How do you know when your AI feature is wrong?
Engineering
Engineering

How do you know when your AI feature is wrong?

Most teams ship an AI feature and have no answer past spot-checking. What an evaluation harness actually contains, and the failure it usually misses.

Don't put a model where you need a reproducible answer
Engineering
Engineering

Don't put a model where you need a reproducible answer

Three systems where I deliberately chose regex and a lookup table over an LLM, and the two questions that decide which one a problem needs.

CLAUDE.md as production infrastructure
Engineering
Engineering

CLAUDE.md as production infrastructure

What a coding agent's context file has to contain when the repos it touches are live, and the day I found the file preventing disasters had no backup.