ErrLookup › Background articles › BadRequestException (HTTP 400) — NestJS 'Bad Request' Errors: Why They Fire and How to Fix Them
BadRequestException (HTTP 400) — NestJS 'Bad Request' Errors: Why They Fire and How to Fix Them
BadRequestException is the NestJS exception class that maps to HTTP 400 Bad Request, thrown when an incoming request is malformed, references a missing resource, violates a domain rule, or trips a security gate. Across cal.com, Hoppscotch, and Immich it is the most common client-side rejection a developer meets on POST/GET/PATCH calls — for input-validation failures, credential lookups, OAuth flow checks, and catch-all wrappers that re-throw upstream errors as a generic 400. Because the class carries only a free-text message, the same 400 status can mean anything from 'you sent a bad field' to 'the server swallowed an internal failure', so reading the message string and the surrounding logs is essential.
Distilled from 243 documented records across 4 repositories.
Background
BadRequestException is a built-in NestJS exception that the framework's global exception filter serializes into an HTTP 400 response body. All four repositories in this family (calcom/cal.diy, hoppscotch/hoppscotch, immich-app/immich) are NestJS applications, so the class is imported from @nestjs/common and constructed as new BadRequestException(message), optionally with a cause. From the caller's side it looks identical regardless of trigger: a JSON error whose message field is whatever string the throw site supplied. That uniformity is both the strength and the weakness of the family — the status is stable and cacheable, but the message is the only signal a client has about what went wrong, and several records show the message being misleading or empty of root cause.
The family exists because HTTP 400 is the spec-correct way to tell a client 'fix your request and try again'. The honest uses are input validation: missing email or code (record 1), a malformed ISO-8601 date (record 2), an inverted or oversized time window (records 11 and 24), a job name outside the ManualJobName enum (record 14), or a slotDuration not in the event type's allowed array (record 25). In these cases the 400 is genuinely the client's bug and the message names the offending field. The same class is also used as a domain-rule gate: a unified-calendar action limited to Google Calendar (record 15), a check that prevents a team from accumulating duplicate Google Meet credentials (record 17), a guard that blocks booking-redirect cycles (record 26), and an API-key permission-escalation check that refuses to mint a broader-scope key from a limited one (record 22). These are policy rejections, not malformed input, but the framework still reports them as 400.
A second, more error-prone pattern uses BadRequestException as a catch-all that swallows an upstream failure and re-throws a friendlier message. IcsFeedService.save (record 3) logs the real exception and throws 'Could not add ICS feeds' without chaining the cause; AppleCalendarService.saveCalendarCredentials (record 18) stringifies the inner error into the message, discarding stack and cause; the SMTP verifier (record 20) wraps with the original as cause; PrivateLinksService.createPrivateLink (record 5) forwards the inner Error.message verbatim, which can leak a Prisma constraint text. The most extreme variant is in cal.com's VerificationAtomService (records 0, 1, 6): the catch predicates compare upstream Error messages against the literal strings 'invalid_code' and 'BAD_REQUEST', but the upstream actually throws 'Invalid verification code' and 'Email and code are required', so both branches are dead and every failure collapses into the generic 'Verification failed'. The 400 status is therefore not always a reliable signal of what the client did wrong.
Finally, several records deliberately report a not-found condition as 400 rather than 404. The asset-metadata endpoint (record 13) throws 400 when a key was never written; the Office 365 (record 19) and Apple Calendar (record 21) connectivity checks throw 400 when no credential row exists; the OAuth /authorize endpoint (record 10) throws 400 for a missing client 'so callers cannot distinguish missing client from bad client id'. This is a library-specific convention, not universal: the records themselves flag the semantic mismatch ('a not found condition is reported as 400 rather than 404'), so a client should not assume a 400 always means retryable bad input.
Common causes
- Missing, empty, or malformed input fields. The request body or query string omits a required field, sends a non-string where a string is expected, or supplies a value the wrong shape. Examples include omitting email or code on the verify endpoint (record 1), sending a Date object or Symbol instead of an ISO-8601 string to an OOO or slots endpoint (record 2), or posting a bare date instead of a full ISO instant for startTime/endTime (records 11, 24). These are the textbook client bugs the 400 status was designed for.
- Referenced resource or credential does not exist (reported as 400). The endpoint resolves the primary object but a required relation or side record is absent: a booking whose eventType was deleted (record 4), an asset with no metadata row for the requested key (record 13), or a user who never completed a calendar connect flow so no Credential row exists (records 19, 21). Several of these are conventionally 400 rather than 404 — the records explicitly call out the semantic mismatch, and the OAuth /authorize endpoint uses 400 for a missing client to avoid leaking existence (record 10).
- Value outside the allowed enum or slug set. The client sent a value that is not a member of a server-side enum or mapping. The manual-jobs switch rejects anything not in ManualJobName (record 14); the unified-calendar gate rejects any slug other than GOOGLE_CALENDAR (record 15); the slots validator rejects a slotDuration not in metadata.multipleDuration (record 25); the booking-location service rejects an integration slug outside the 29-entry apiToInternalintegrationsMapping (record 28); and the mock-server guard rejects an unrecognized workspaceType (record 7). Often the DTO already constrains the value, so hitting this means the client and server enum are out of sync.
- OAuth / OIDC flow integrity violation. The OAuth round-trip is missing a required piece or carries an invalid one: no state query param on the callback (record 23), a redirectUri whose origin is not on the client's allow-list (record 29), an access token in state that is expired or from another environment (record 27), a user who tries to authorize an already-authorized client (record 12), or a provider-echoed error_description when the user denied consent (record 8). The OIDC back-channel logout path fails on signature, issuer, audience, or events-claim mismatch (record 16).
- Upstream failure wrapped and re-thrown as a generic 400. A try/catch around an external call (DAV listCalendars, SMTP verify, Prisma upsert, symmetricEncrypt on an unset key) catches any exception and re-throws a friendly BadRequestException, often losing the original cause. The ICS feeder (record 3) only logs the real error; the Apple calendar saver (record 18) stringifies the reason into the message; the SMTP verifier (record 20) preserves the cause; the private-link creator (record 5) forwards the inner Error.message, which can leak database text. An unset or rotated CALENDSO_ENCRYPTION_KEY is a recurring root cause hidden behind these wrappers (records 3, 18).
- Security or policy constraint rejection. The request is well-formed but violates a guard: an API key attempts to mint a key with permissions it does not itself hold (record 22), a redirect window would create an infinite booking-redirect chain (record 26), a team already has the conferencing credential being added (record 17), or a mock-server workspaceType the guard does not case for reaches the else branch (record 7). These are intentional rejections reported as 400.
- Catch-predicate mismatch producing dead branches or wrong messages. Where the code compares an upstream Error message against a literal string, the comparison can silently miss. In cal.com's VerificationAtomService the predicates check for 'invalid_code' and 'BAD_REQUEST' but the upstream throws 'Invalid verification code' and 'Email and code are required', so both branches are dead and all failures surface as the generic 'Verification failed' (records 0, 1, 6). The slots controller's substring match on 'Invalid time range given' (records 11, 24) is the same shape and is fragile in the same way.
What usually fixes it
- Validate input shape at the DTO boundary with class-validator decorators (@IsNotEmpty, @IsString, @Matches, @IsIn) so missing or malformed fields return a clear 422/400 before the service layer's try/catch ever runs. Several records (1, 2, 25, 28) note this would have prevented the error entirely.
- Pre-check resource and credential existence before calling mutating endpoints: list metadata keys before fetching one (13), call 'save' before 'check' for calendar credentials (19, 21), and confirm a booking's eventType is non-null before reassigning (4). Make connect and authorize actions idempotent with an 'ensure connected' helper so duplicate attempts (12, 17) short-circuit instead of erroring.
- Keep enums, slugs, and integration mappings in sync between client and server. Regenerate the client SDK from the running server's OpenAPI spec (14), import server constants instead of hardcoding slugs (15, 28), and add a unit test that iterates every enum member against the guard branches (7) so a new enum value cannot reach an unreachable else branch.
- Recover swallowed root causes by reading the server log line right next to the throw. The ICS feeder (3), Apple calendar saver (18), SMTP verifier (20), and back-channel logout (16) all log the original error or attach it as cause; the HTTP message alone is often incomplete. For the encryption-key-driven failures (3, 18), confirm CALENDSO_ENCRYPTION_KEY is set and identical across all instances before debugging anything else.
- Enter OAuth flows only through the server-generated auth-url endpoint so state, redirectUri, and the embedded access token are produced server-side and survive the round-trip (23, 27, 29). Register every redirect URI with exact scheme, host, port, and path, and ensure the user's access token outlives the OAuth round-trip.
- Where the code matches upstream error messages by string, align the predicate with the real message or — better — throw and catch a typed ErrorWithCode checked with instanceof. The dead verification branches (0, 1, 6) and the substring-matching slots controllers (11, 24) are fragile precisely because they depend on literal upstream text; a typed error makes the branches reachable and distinguishable.
Go deeper
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
Documented occurrences
- Invalid verification code (calcom/cal.diy)
- Email and code are required (calcom/cal.diy)
- Invalid Date. (calcom/cal.diy)
- Could not add ICS feeds, try using private ics feed. (calcom/cal.diy)
- Event type with id=${booking.eventTypeId} was not found in the database (calcom/cal.diy)
- ${error.message} (calcom/cal.diy)
- Verification failed (calcom/cal.diy)
- Invalid workspace type for mock server. (hoppscotch/hoppscotch)
- {error_description} (calcom/cal.diy)
- Listed cals and URLs mismatch: ${listedCals.length} vs. ${urls.length} (calcom/cal.diy)
- OAuth client with ID '${clientId}' not found (calcom/cal.diy)
- Invalid time range given - check the 'startTime' and 'endTime' query parameters. (calcom/cal.diy)
- User with id=${userId} has already authorized client with id=${clientId}. (calcom/cal.diy)
- Metadata with key "${key}" not found for asset with id "${id}" (immich-app/immich)
- Invalid job name (immich-app/immich)
- ${action} is currently only available for Google Calendar. Office 365 and Apple support is coming soon. (calcom/cal.diy)
- Error backchannel logout: token validation failed (immich-app/immich)
- Google Meet is already connected for this team. (calcom/cal.diy)
- Could not add this apple calendar account: ${reason} (calcom/cal.diy)
- Credentials for office_365_calendar not found. (calcom/cal.diy)
…and 223 more across the corpus — use search.
Honest provenance: generated on 2026-08-12 from AI-assisted analysis of the linked records. See how records are made.