Skip to content

fix(workspace): resend an idempotent Nextcloud request once when it lands on a socket the server already closed (workspace 0.2.3) - #537

Merged
kh0pper merged 1 commit into
mainfrom
fix/workspace-calendar-t10-i3
Oct 21, 2026
Merged

kh0pper merged 1 commit into
mainfrom
fix/workspace-calendar-t10-i3

Conversation

@kh0pper

@kh0pper kh0pper commented Oct 21, 2026

Copy link
Copy Markdown
Owner

What was failing

tests/workspace-calendar.test.js "T10-I3: after_etag comes from the PUT's ETag…" failed in CI (seen on #535, and once in a local full suite) with Cannot read properties of undefined (reading 'uid'): ws_cal_create_event answered workspace_unreachable ("connection failed").

Root cause

It depends on load, not on the date or on test order.

  • The test right before it (T10-I2 "a rule too dense to expand…") runs a synchronous repeat expansion. On the CI runner it took 6.6 s; locally it takes about 2 s.
  • During that time the event loop is blocked. The fake Nextcloud's keep-alive timeout is Node's default 5 s, so the server closes the idle pooled socket. fetch never sees the close in time and sends T10-I3's first request (the calendar PROPFIND) on that socket. The request fails with ECONNRESET, and ncFetch maps that to workspace_unreachable.
  • Reproduced: blocking the loop for 3 to 7 s at the start of T10-I3 fails it every time (the logged cause is fetch failed / ECONNRESET on the PROPFIND). Blocking for 2.5 s or less passes.

The product has the same hazard. A real Nextcloud (Apache) also drops idle keep-alive sockets after a few seconds, and the same dense expansion blocks the real Workspace server too, so the next tool call could fail with "Crow Workspace did not answer".

Fix

ncFetch (bundles/workspace/server/nc/http.js) resends a request once when it fails with a stale-socket reset (ECONNRESET / EPIPE / UND_ERR_SOCKET). The resend goes on a fresh connection, and only for methods that are safe to send twice: GET, HEAD, OPTIONS, PROPFIND, REPORT, SEARCH, PUT and DELETE. A conditional PUT whose first attempt did land answers 412, which becomes changed_concurrently, never a double write. Some failures are never resent:

  • POST, MOVE, MKCOL, PROPPATCH and LOCK.
  • Timeouts.
  • Refused connections.

Workspace manifest bumped to 0.2.3 (registry regenerated).

Tests

  • New tests/workspace-nc-http.test.js uses a server that resets the second request on a kept-alive socket. It checks that each idempotent method is resent once on a new connection, that POST and MOVE go out exactly once and fail as workspace_unreachable, and that a refused connection is not retried. The resend cases fail without the fix and pass with it.
  • tests/workspace-calendar.test.js passes, including with a 6 to 7 s forced event-loop block before T10-I3 (it failed 100% that way before the fix).
  • Full suite: 10426 tests, 10391 pass, 0 fail, 35 skipped (taskset -c 0-11,16-27 npm test).

https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp

…ands on a socket the server already closed (workspace 0.2.3)

A long synchronous repeat expansion (T10-I2's dense rule took 6.6 s on the CI
runner) blocks the event loop past the server's 5 s keep-alive timeout; fetch
then reuses the closed pooled socket, the next request dies with ECONNRESET and
ncFetch answered workspace_unreachable. That was the T10-I3 calendar flake, and
the real Workspace server has the same hazard against Nextcloud.

ncFetch now resends once, on a fresh connection, for ECONNRESET/EPIPE/
UND_ERR_SOCKET on GET/HEAD/OPTIONS/PROPFIND/REPORT/SEARCH/PUT/DELETE only.
POST/MOVE/MKCOL/PROPPATCH/LOCK, timeouts and refused connections are never
resent. New tests/workspace-nc-http.test.js covers both sides.

Claude-Session: https://claude.ai/code/session_01N9fXzvNy8rCEArj1y5p6fp
@kh0pper
kh0pper merged commit 6a3cd8d into main Oct 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant