Troubleshooting
Find the cause and fix for common startup, OAuth, Calendar API, sync, watch, and management API errors in Calendarpit.
When a request fails, Calendarpit returns an explicit status code and error message. It does not silently ignore unsupported fields or parameters.
To investigate a failure, open API history (http://127.0.0.1:4173/history) in the admin UI:
- It shows the latest 200 Calendar API and OAuth exchanges.
- Each entry records the HTTP method, path, status code, headers, and masked request and response bodies.
- The Errors toggle filters for responses with status
400or higher.
Each entry on this page follows the same pattern: the exact Error output (when there is one), the likely Cause, and the Fix.
Error response formats
Google API error envelope
Calendar API routes (/calendar/v3/*), /userinfo, and management API routes (/__calendarpit/*) return the Google API error envelope:
{
"error": {
"code": 404,
"message": "Event not found.",
"errors": [
{
"domain": "calendarpit",
"reason": "notFound",
"message": "Event not found."
}
]
}
}domainis"calendarpit"on all top-level errors. Per-calendar errors inside afreebusy.queryresponse use"domain": "global".401responses include theWWW-Authenticate: Bearerheader.400validation errors without a more specific reason use"reason": "invalidParameter".
OAuth error format
OAuth routes (/oauth/* and /oauth2/*) return the OAuth error format. Most errors are HTTP 400, with these exceptions:
- Unimplemented paths return
501. - Too many pending consent requests return
429. - A cross-origin consent submission returns
403.
{
"error": "invalid_grant",
"error_description": "Invalid, expired, or mismatched authorization code."
}Validation errors on GET /oauth/authorize are returned directly in the browser as HTTP 400 JSON. Calendarpit does not redirect invalid authorization requests back to redirect_uri.
Starting Calendarpit
CLI startup failures print Calendarpit: <message> to stderr and exit with code 1. For Docker, inspect the server output with docker compose logs calendarpit.
Port already in use
- Error:
Calendarpit: Failed to start server. Is port 4173 in use? - Cause: Another process is listening on port
4173. - Fix: Stop that process, or choose another port with
--port <n>. With--port 0, read the actualAdminURL. In Docker, change the host side of the mapping, for example127.0.0.1:4174:4173, and open port4174.
Database already in use
- Error:
Calendarpit: Database is already in use: <path>. Use a separate path or :memory:. - Cause: Another process holds the lock on the same SQLite file.
- Fix: Stop that process, or give each process its own
--databasepath or:memory:. See Storage and restarts.
Fixture file not found
- Error:
Calendarpit: ENOENT: no such file or directory, open '<path>' - Cause: The fixture path does not exist in the current working directory or inside the container.
- Fix: Use a valid absolute path. In Docker, mount the file and pass its container path. See Fixtures.
Fixture fails validation
- Error:
Calendarpit: Each user with calendars must have exactly one primary calendar. - Cause: The startup fixture fails validation.
- Fix: Give each user with calendars exactly one
"primary": truecalendar. See the fixture rules.
Fixture edits have no effect after restart
- Cause: Startup fixtures only seed a new database. Existing data and the reset baseline are retained.
- Fix:
- To replace current data, load the file via Settings → Test data or
POST /__calendarpit/fixtures. - To use a new reset baseline, start with a new database path or
:memory:.
- To replace current data, load the file via Settings → Test data or
Data is missing after changing how Calendarpit starts
- Cause: One of the following:
- The working directory changed.
- A different database path or volume was used.
- The previous session used
:memory:.
- Fix: Check
Data <path>in startup output and Settings → Test data. Use the same absolute database path or Docker volume.
The browser, backend, or webhook receiver cannot connect in Docker
- Cause: Loopback points to the requesting container itself. A browser cannot resolve a Compose service name.
- Fix: Use the host-published URL in the browser and the service name between containers. For webhook receivers, configure
CALENDARPIT_WATCH_HOSTS. See container networking.
A CALENDARPIT_* value seems ignored
- Cause: A CLI flag overrides the environment variable, or the variable was set on the host but not passed to Docker.
- Fix: Remove the overriding flag, or pass the variable with
docker run -eor Composeenvironment. When you change the container port, also update its mapping andCALENDARPIT_PORTfor the health check.
Invalid option value, unknown option, or unknown command
- Cause: The CLI received an invalid flag, empty value, or unsupported subcommand.
- Fix: Run
calendarpit --help. Onlyserveis supported. Ports must be integers from0to65535.
OAuth
A real Google consent screen appears
- Cause: Your app's authorization URL still points to Google (
accounts.google.com). - Fix: Set the authorization URL to
http://127.0.0.1:4173/oauth/authorize. See Connect your app.
Token exchange or refresh contacts Google
- Cause: Your OAuth client still uses Google's token endpoint. Setting the Calendar API endpoint does not redirect OAuth calls.
- Fix: Override the token URL to
http://127.0.0.1:4173/oauth/token. If you use/oauth/revokeand/userinfo, override them too. See Local OAuth.
Google's default OAuth paths return 501
- Error:
501 unsupportedFeatureon/oauth2/...or/oauth/tokeninfo - Cause: Your SDK is calling Google's default OAuth paths:
/oauth2/v4/token,/oauth2/v3/userinfo, or/oauth/tokeninfo. - Fix: Override the endpoints explicitly to
/oauth/authorize,/oauth/token,/oauth/revoke, and/userinfo.
Unregistered redirect URI
- Error:
invalid_request:Unregistered redirect URI. - Cause:
redirect_uridoes not exactly match an entry inclients[].redirectUris.- The match covers
localhostvs127.0.0.1, port, path, and trailing slash. - Changing
SAMPLE_PORTfrom4175also causes a mismatch with the built-insample-appclient.
- The match covers
- Fix: Copy the exact
redirect_uristring intoclients[].redirectUrisin your fixture.
Unknown client
- Error:
invalid_client:Unknown client. - Cause: One of the following:
client_idis not registered in the fixture.POST /oauth/tokenused a JSON body instead ofapplication/x-www-form-urlencoded. With HTTP Basic authentication, a JSON body returnsunsupported_grant_typeinstead.
- Fix: Register the client in your fixture. Send token requests as
application/x-www-form-urlencoded.
Invalid client secret
- Error:
invalid_client:Invalid client secret. - Cause:
client_secretis missing or wrong. Calendarpit requires a client secret on every token request, including PKCE flows. - Fix: Send the matching
client_secretin the form body or via HTTP Basic authentication.
Invalid authorization code
- Error:
invalid_grant:Invalid, expired, or mismatched authorization code. - Cause: One of the following:
- The code is older than 60 seconds.
- The code was already used once.
redirect_uriis missing or differs from the one sent to/oauth/authorize.- The client differs.
- The connection was revoked.
- The server restarted.
- Fix: Exchange the code once within 60 seconds. Use the exact
redirect_uriand client used at/oauth/authorize.
Invalid PKCE verifier
- Error:
invalid_grant:Invalid PKCE verifier. - Cause:
code_verifierdoes not match theS256code_challenge, or is not 43–128 characters of[A-Za-z0-9._~-]. - Fix: Send the original verifier string used to compute the
S256challenge.
Invalid refresh token
- Error:
invalid_grant:Invalid refresh token. - Cause: One of the following:
- The connection was revoked.
- The user was deleted.
- The workspace was reset or a fixture was loaded.
- An in-memory server restarted.
- The token belongs to another client.
- Fix: Run the authorization flow again to create a new connection.
state is missing or mismatches on callback
- Cause: One of the following:
statewas omitted from/oauth/authorize. It is echoed only when sent.- Your app started on
localhostand received the callback on127.0.0.1. Cookies holdingstateare not shared across hostnames.
- Fix: Pass
stateon/oauth/authorize. Use the same hostname (127.0.0.1orlocalhost) for both your app and itsredirect_uri.
Unsupported or missing scope
- Error:
invalid_scope:Unsupported or missing scope. - Cause:
scopeis empty or includes an unsupported scope, such ascalendar.events.ownedor Drive scopes. - Fix: Request only supported scopes.
Scope changes on refresh
- Error:
invalid_scope:Refresh scope changes are unsupported. - Cause: A
refresh_tokenrequest included ascopeparameter that does not exactly match the original grant string. Scope order counts. - Fix: Omit
scopewhen refreshing a token.
No refresh_token in the token response
- Cause: One of the following:
access_type=offlinewas not sent on/oauth/authorize.- The response is from a
refresh_tokengrant. Refresh responses return a newaccess_tokenonly, and the original refresh token stays valid.
- Fix: Add
access_type=offlineto/oauth/authorize, and keep the initialrefresh_token.
Unsupported authorization parameters
- Error:
unsupported_featureon/oauth/authorize - Cause: The request included one of the following:
prompt=noneinclude_granted_scopes=true- An unsupported parameter such as
nonce,login_hint,hd, orenable_granular_consent
- Fix: Remove unsupported parameters. Use interactive consent (
prompt=consentorprompt=select_account).
Invalid PKCE challenge
- Error:
invalid_request:PKCE requires S256.orMissing PKCE challenge. - Cause: One of the following:
code_challengewas sent withoutcode_challenge_method=S256(includingplain).code_challenge_methodwas sent withoutcode_challenge.code_challengeis not 43 base64url characters.
- Fix: Use
code_challenge_method=S256with a 43-character base64url challenge.
Consent request expired or already used
- Error:
invalid_request:Consent request expired or already used. - Cause: One of the following:
- The consent page was left open longer than 5 minutes.
- The consent page was submitted twice.
- The server restarted before submission.
- Fix: Start a fresh authorization request at
/oauth/authorize.
Too many pending consent requests
- Error:
429 rateLimitExceeded:Too many pending consent requests. - Cause: 1,000 consent requests are already pending. Each expires after 5 minutes. A loop in tests usually causes this.
- Fix: Complete pending consent requests or stop opening new ones, or reset the workspace.
Calendar API and SDKs
Invalid or expired access token
- Error:
401 authError:Invalid or expired access token. - Cause: One of the following:
- The
Authorization: Bearerheader is missing or malformed. - The access token expired (1-hour TTL).
- The connection was revoked.
- The workspace was reset or a fixture was loaded.
- An in-memory server restarted.
- A real Google token was sent.
- The
- Fix: Refresh the token at
/oauth/token. After a reset, fixture load, or revocation, authorize again.
Insufficient scope
- Error:
403 insufficientPermissions:Insufficient scope. - Cause: The token lacks a required scope. For example:
- Writing events with a
.readonlyscope. - Calling
calendarList.listwith onlycalendar.events. - Calling
/userinfowithoutopenid,email, orprofile.
- Writing events with a
- Fix: Request a scope that covers the endpoint and re-authorize. See Local OAuth.
Unsupported API endpoint or method
- Error:
501 unsupportedFeature:Unsupported API endpoint or method.This includes doubled/calendar/v3/calendar/v3/...paths. - Cause: One of the following:
- You called an unimplemented Google endpoint:
acl,settings,colors,calendars.insert, orevents.move. - You sent
HEADto an API path. Some paths answerHEADdifferently, as listed below. - You sent
POSTto/users/me/calendarList/{calendarId}. - The SDK base URL is misconfigured, so the SDK doubled
/calendar/v3.
- You called an unimplemented Google endpoint:
- Fix: Check Calendar API compatibility. Set the SDK base URL as shown in the table below. See SDK examples.
HEAD returns a different error on these paths:
- Event paths respond with
405 methodNotAllowed. /calendars/{calendarId}returnsUnsupported calendar operation.- An existing entry at
/users/me/calendarList/{calendarId}returnsUnsupported calendar list operation.
| SDK | Base URL setting |
|---|---|
JS/TS (@googleapis/calendar) | Set rootUrl to http://127.0.0.1:4173/. |
| Java | Set rootUrl to the origin and servicePath to calendar/v3/. |
| Python, Go, and C# | Include /calendar/v3/. |
Unsupported calendar or calendar list operation
- Error:
501 unsupportedFeature:Unsupported calendar operation.orUnsupported calendar list operation. - Cause: One of the following:
- You called
PUT,PATCH,DELETE, orHEADon/calendar/v3/calendars/{calendarId}. - You called an unsupported method such as
HEADorOPTIONSon an existing entry at/calendar/v3/users/me/calendarList/{calendarId}.POSTthere returnsUnsupported API endpoint or method.instead.
- You called
- Fix: Manage calendars in the admin UI or via
/__calendarpit/calendars. For per-user display settings, usecalendarList.patch. See Test control API.
Calendar not found
- Error:
404 notFound:Calendar not found. - Cause: One of the following:
- The calendar ID is unknown.
- The calendar is owned by a different test user.
- You used
primaryfor a user with no calendars. - The calendar was removed from the user's list with
calendarList.delete. This applies to calendar-list methods only.
- Fix: Use a calendar ID owned by the authenticated user, or authorize as that calendar's owner. Restore a removed list entry with
calendarList.insert.
Event not found
- Error:
404 notFound:Event not found. - Cause: One of the following:
- The event ID is unknown in that calendar.
- You called
POST .../events/importorPOST .../events/quickAdd(withouttext). Calendarpit treatsimportorquickAddas an event ID.
- Fix: Check the event ID. Use
events.insertinstead ofimportorquickAdd.
Unsupported event method
- Error:
405 methodNotAllowed:Unsupported event method.orUnsupported instances operation. - Cause: The HTTP verb is invalid for the event route. For example:
HEADDELETEon/eventsPOSTon an existing/events/{eventId}- A write to
/instances
- Fix: Use the supported HTTP method and path.
Unsupported query parameter
- Error:
400 unsupportedFeature:Unsupported query parameter: <key> - Cause: The query parameter is not on the endpoint allowlist. Examples include
fields,q,eventTypes,sendNotifications,supportsAttachments,maxAttendees, andtext. - Fix: Remove the parameter. Disable automatic
fieldspartial-response masks in your SDK.
Unsupported field
- Error:
400 unsupportedFeature:Unsupported field: <key> - Cause: The request body contains an unsupported field. For example:
eventType,attachments,source, orhtmlLinkon events.- An
items[]entry with keys other thanidon FreeBusy.
- Fix: Strip unsupported fields before sending the request. See Calendar API compatibility.
Non-JSON response format
- Error:
400 invalidParameter:Only JSON responses are supported. - Cause: The query included
alt=mediaoralt=proto. - Fix: Omit
altor usealt=json.
Timestamp without an offset
- Error:
400:Expected an RFC3339 timestamp with an offset.ordateTime requires an offset or timeZone. - Cause: One of the following:
timeMin,timeMax,updatedMin, or FreeBusy bounds lackZor±HH:MM.- An event
dateTimehas neither an offset nortimeZone.
- Fix: Add
Zor a±HH:MMoffset, or supplytimeZoneon the eventstart/end.
Invalid or ambiguous local time
- Error:
400:Invalid or ambiguous local time; supply an explicit offset. - Cause: An offset-less
dateTimefalls inside a DST gap or overlap intimeZone. - Fix: Include an explicit numeric offset on
dateTime.
Recurring event time zone or UNTIL errors
- Error:
400:Recurring timed events require start.timeZone.orTimed recurrence UNTIL must be UTC. - Cause: A timed recurring event has no
start.timeZone, or itsUNTILvalue does not end withZ. - Fix: Set
start.timeZone. Write timedUNTILtimestamps in UTC (...Z). See Events, recurrence and time.
Unsupported recurrence rule or expansion
- Error:
400 unsupportedFeatureon recurrence rules or expansion - Cause: One of the following:
- The rule uses
HOURLYorMINUTELY. - The rule uses an unsupported part:
BYHOUR,BYWEEKNO, orBYYEARDAY. - The rule includes another iCalendar line (
DTSTART). - An occurrence falls in a DST gap.
- An unbounded rule is expanded without
timeMax. - The expansion exceeds 10,000 instances or 100 years.
- The rule uses
- Fix: Use supported
DAILY,WEEKLY,MONTHLY, orYEARLYrules. SupplytimeMaxand a narrower window. See Events, recurrence and time.
orderBy=startTime without singleEvents
- Error:
400:startTime ordering requires singleEvents=true. - Cause:
orderBy=startTimewas sent withoutsingleEvents=true. - Fix: Add
singleEvents=trueto theevents.listquery.
Event is not a recurring series
- Error:
400:Event is not a recurring series. - Cause:
events.instanceswas called on a non-recurring event. - Fix: Call
events.instancesonly on recurring series masters.
Read-only field changed
- Error:
400:<key> is read-only. - Cause: One of the following:
- A Calendar API write changed
organizer,creator,iCalUID,recurringEventId, ororiginalStartTime. - A
calendarListwrite changedsummary,timeZone,accessRole,primary,kind,deleted, orconferenceProperties.
- A Calendar API write changed
- Fix: Omit the field or send the existing value unchanged.
- For a per-user calendar name, set
summaryOverride. - To set custom organizer metadata, use Fixtures or the Test control API.
- For a per-user calendar name, set
conferenceData without conferenceDataVersion
- Error:
400:conferenceData requires conferenceDataVersion=1. - Cause: The event payload includes
conferenceDatawithout?conferenceDataVersion=1. - Fix: Add
conferenceDataVersion=1to the query string.
Cannot remove the primary calendar
- Error:
403 cannotDeletePrimaryCalendar:Cannot remove the primary calendar from the calendar list. - Cause:
calendarList.deletewas called on the user's primary calendar. - Fix: Keep the primary calendar in the list. To hide it, use
calendarList.patchwithhidden: trueorselected: false.
Invitee edits are rejected
- Error:
403 forbiddenForNonOrganizer - Cause: An invitee editing an invitation copy did one of the following:
- Used
PUT. - Changed shared event fields.
- Edited another attendee.
- Used
- Fix: Use
PATCH. Update only your ownresponseStatus/comment,reminders,colorId,transparency, orextendedProperties.
Duplicate event or calendar list entry
- Error:
409 duplicatewith one of these messages:Duplicate event ID.Invitation event ID conflicts with an existing local event.Calendar already exists in the calendar list.
- Cause: One of the following:
events.insertreused an existing ID in that calendar.- An attendee invitation copy matched an event ID in the attendee's primary calendar with a different
iCalUID. For example, the existing event'siCalUIDwas set through a fixture or the Test control API. An existing event with the defaultiCalUIDis overwritten instead. calendarList.insertwas called for a calendar already in the list, includingprimary.
- Fix: Use a unique event ID. Call
calendarList.insertonly for a calendar that is not in the list.
Event is already deleted
- Error:
410 deleted:Event is already deleted. - Cause:
events.deletewas called on an event that is alreadycancelled. - Fix: Treat
410on delete as already deleted.
etag mismatch
- Error:
412 conditionNotMet:Event has changed.orCalendar list entry has changed. - Cause: The
If-Matchheader does not match the currentetagof the event or calendar-list entry. - Fix: Re-fetch with
events.getorcalendarList.getand retry.
Request body too large
- Error:
413 payloadTooLarge:Request body too large. - Cause: The request body exceeds 1,048,576 bytes (1 MiB).
- Fix: Reduce the payload size.
maxResults out of range
- Error:
400:maxResults must be between 1 and 2500. - Cause:
maxResultsis0, negative, or greater than2500. - Fix: Set
maxResultsbetween1and2500.
Python SDK fetches discovery documents from Google
- Cause:
googleapiclient.discovery.buildtried to download discovery metadata from Google. - Fix: Use
google-api-python-client2.x withstatic_discovery=Trueandcache_discovery=False. Do not set a remote discovery URL. See SDK examples.
Incremental sync and pagination
Sync token is invalid or expired
- Error:
410 fullSyncRequired:Sync token is invalid or expired. Perform a full synchronization. - Cause: One of the following:
- The
syncTokenis older than 7 days. - The
syncTokenwas evicted because the workspace has more than 100 tokens. - The
syncTokenbelongs to another calendar or user. - A reset, fixture load, or in-memory restart cleared the
syncToken.
- The
- Fix: Discard local sync state and run a full
events.listwithoutsyncToken. See Incremental sync.
Sync query options changed
- Error:
400:Sync query options must match the initial synchronization. - Cause:
singleEventsor the effectivetimeZonediffers from the initial sync. WhentimeZonewas omitted, a calendar time zone change also counts. - Fix: Repeat the exact
singleEventsandtimeZoneoptions from the initial full sync.
Invalid syncToken combination
- Error:
400with one of these messages:<key> cannot be combined with syncToken.Incremental synchronization must include deleted events.instances does not support syncToken.
- Cause: One of the following:
syncTokenwas combined withtimeMin,timeMax,orderBy,updatedMin,iCalUID, or extended property filters.syncTokenwas sent withshowDeleted=false.syncTokenwas used onevents.instances.
- Fix: Send only
syncTokenonevents.list. You may also send matchingsingleEvents,timeZone,maxResults,pageToken, orshowDeleted=true.
Invalid or expired page token
- Error:
410 invalidPageToken:Invalid or expired page token.orInvalid or stale page token. - Cause: The page token is no longer valid.
- An event
pageTokenexpired (1-hour TTL), was evicted (over 1,000 frozen pages), or was invalidated because query options changed or the server restarted or reset. - A
calendarListpageTokenwas used after any workspace data write.
- An event
- Fix: Restart listing from the first page.
Watch notifications
Watch host is not allowed
- Error:
400:Watch host is not allowed. Configure CALENDARPIT_WATCH_HOSTS explicitly. - Cause: The callback
addresshostname is not127.0.0.1,[::1], orlocalhost. For example, it ishost.docker.internalor a container service name. - Fix: Start Calendarpit with
CALENDARPIT_WATCH_HOSTS=<host1>,<host2>. List hostnames only, without ports. See Watch notifications.
Invalid watch callback URL
- Error:
400:Invalid watch callback URL. - Cause:
addressis nothttp:orhttps:, or includes credentials or a#fragment. - Fix: Use a plain
http://orhttps://callback URL.
Invalid channel fields
- Error:
400with one of these messages:Only web_hook channels are supported.Invalid channel ID.Invalid channel token.Invalid expiration.
- Cause: One of the following:
typeis not"web_hook".iddoes not match[A-Za-z0-9_-]{1,64}.tokenexceeds 256 characters or contains newlines.expirationis not a future Unix millisecond integer.
- Fix: Fix the invalid field in the
events.watchorcalendarList.watchrequest body.
Duplicate channel ID
- Error:
409 duplicate:Duplicate channel ID. - Cause: Another active watch channel in the workspace already uses that
id. - Fix: Generate a new unique
id(such as a UUID) for each channel.
Watch succeeds, but no notifications arrive at your app
- Cause: Delivery to your webhook receiver failed. After 3 attempts, the delivery is marked
failed. An attempt fails when the receiver:- Is unreachable.
- Takes more than 2,000 ms.
- Returns a status other than
200,201,202, or204. 3xx redirects are not followed.
- Fix: Open Watch (
http://127.0.0.1:4173/watch) in the admin UI to inspect the delivery log status and HTTP status code. In tests, callPOST /__calendarpit/deliveries/drainto wait for pending deliveries.
Notifications stop arriving after restart, reset, or revocation
- Cause: One of the following:
- Restarting cancels queued deliveries.
- An in-memory restart drops channels.
- A restart with a disallowed callback host drops channels.
- Resetting, loading a fixture, or revoking the connection stops channels.
- Deleting a calendar stops the event channels on that calendar.
- The channel expired. Channels expire after 24 hours by default, and after 7 days at most.
- Fix: Re-fetch state after a restart. If the channel was stopped or expired, register a new watch channel.
Channel not found on channels.stop
- Error:
404 notFound:Channel not found.onchannels.stop - Cause: One of the following:
- The
resourceIdis wrong. - The channel ID is unknown.
- The channel was already stopped, or pruned after expiry.
channels.stopwas called with a token from a different connection than the one that created the channel.
- The
- Fix: Call
channels.stopwith the creating connection's token and the matchingresourceId. An expired channel that has not yet been pruned still returns204. See Watch notifications.
Management API
Management writes require JSON
- Error:
415 unsupportedMediaType:Management writes require application/json. - Cause: A
POST,PATCH,PUT, orDELETErequest to/__calendarpit/*was sent withoutContent-Type: application/json. This includes bodyless calls like/resetor/deliveries/drain. - Fix: Include
-H 'Content-Type: application/json'on every management write request. See Test control API.
Cross-origin management request
- Error:
403 forbidden:Cross-origin management requests are not allowed. - Cause: A browser script on another origin called
/__calendarpit/*. - Fix: Call
/__calendarpit/*from your test runner, CLI, or backend, without a cross-originOriginheader. Or use the admin UI.
State response posted as a fixture
- Error:
400 unsupportedFeature:Unsupported field: storageonPOST /__calendarpit/fixtures - Cause: You posted a
GET /__calendarpit/stateresponse directly as a fixture. - Fix: Post a valid fixture object containing only
users,calendars,events, andclients. Include the clientsecretvalues. See Fixtures.