A backend with setProps that rejects unsupported for this path got 501 while a
backend without setProps got 403, for the same inability. 403 is the answer
kept: WebDAV allows it for a property the server will not store, where 501 says
PROPPATCH itself is unimplemented, which is not what the server means.
Refs #179
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
copyRecursive predated @webnet/vfs/fallback and disagreed with it: it did not
reject an overlapping source and destination, left a partial destination behind
on failure, and tolerated an existing directory mid-tree rather than replacing
the destination.
The policy is on-unsupported, so a native copy or move that rejects unsupported
for this path falls back to the traversal. That code guarantees nothing changed,
and WebDAV has no way to tell the client which of the two it got, so refusing
would only lose it the operation.
The destination is now statted twice per request, once by the handler to answer
201 or 204 and once by the fallback to decide whether to clear it.
Refs #178, #179
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The SFTP server drained the prefix inside openReadStream, before the reader was
handed back, holding the SSH_FXP_READ reply open for the whole discard. FTP had
its own skipBytes for the same job. Both use readFileRangeFallback now, which
windows lazily and cancels the source once the window is filled, and skipBytes
goes with its only caller.
Refs #178, #179
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
http-static had its own boundedStream for the same job readFileRangeFallback
does. Both servers now use the shared one with the on-unsupported policy: a
Range a backend could serve is not refused, so a native readFileRange that
rejects unsupported for this path falls back like an absent one does.
Refs #178, #179
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opting out skipped the test entirely, leaving the path with no coverage. The
tri-state capability with a forbidden alias keeps the assertions that the call
rejects and the properties are unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each package ran the suite against a server backed by a full MemoryVFS, so
the servers' fallback paths were never entered. A second run reduces the
server's filesystem to the required operations while leaving the client under
test unchanged; it fails on #177 without the previous commit. Where a server
answers rather than shims — WebDAV 403 for PROPPATCH, FTP 502 and SFTP
SSH_FX_OP_UNSUPPORTED for rename — the run declares that instead of opting
out, which is the decision #179 revisits.
Fixes#181
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fallback branch discarded range.start and streamed the file from byte
zero while the response advertised the requested window, so a client asking
for bytes 5-9 got bytes 0-4 and stored them at the wrong offset.
readFileRangeFallback windows the stream and calls the native operation when
there is one.
Fixes#177
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
withoutOptional and unsupportedOptional replace the local copies in the
fallback tests; the protocol packages need the same reduction to put their
servers' fallback paths under the suite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every one of these callers needs the listing to answer the request, and the
stat plus readdir it falls back to are required operations, so there is no
reason to stop at an implementation that has the combined call but cannot serve
it for this path. The default policy stops there because for most operations a
fallback is materially different from the native one; for this one it is only
slower.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A stream that fails after the native call resolved is deliberately not retried,
and cancelling the fallback's output while a read is in flight must still
release the source. Both were argued from the code rather than demonstrated.
The root path overlaps every other path, so it is rejected at either end of a
copy; that followed from the guard without being stated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An implementation that answers unsupported at runtime was not expressible: the
capability flags only said present or absent, so the suite could not check that
such a rejection carries the right code and leaves the filesystem alone.
The fallbacks are then run against the same suite with the optional operations
first removed and then rejecting, which is what shows they can stand in for
them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both clients failed the boundary behaviour the conformance suite now requires.
WebDAV answers a range at or past the end of a file with 416, which RFC 9110
defines as unsatisfiable and the client reported as an unexpected status; at
this level the window is simply empty. FTP carries no end bound on REST, so an
end below the start transferred from the start to the end of the file instead
of nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Serving a byte range needs an empty window and an unanswerable request to be
distinguishable, so a start at or past the end of a file and an end below the
start return an empty stream rather than failing. The suite did not say so, and
NodeVFS passed both bounds to createReadStream, which rejects an inverted range
with a plain RangeError.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The optional operation arrived with three byte-identical copies of the same
fallback, one per call site.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Callers of an optional AsyncVFS operation each had to decide what to do when
an implementation lacked it, so the same fallbacks were written repeatedly and
diverged. withFallbacks presents every optional operation as available, so a
consumer can call it without branching on method presence, and the operations
that cannot be built from the required ones say so with the unsupported code
rather than appearing absent.
The policy chooses when a fallback may stand in. Only an unsupported rejection
ever triggers one: any other failure means the operation was attempted, and
retrying it another way would hide that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each client already had a native response meaning the operation does not exist
here, and each discarded it: SFTP OP_UNSUPPORTED and FTP 502 surfaced as bare
protocol errors, WebDAV 501 hit the unexpected-status branch, and SMB2
STATUS_NOT_SUPPORTED reached callers as forbidden, which claims a permission
problem the server never reported.
Without this a server emits the status and its own client throws the meaning
away, so no caller can act on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Optional AsyncVFS operations could only be declared by method presence, which
is global to an instance and cannot express a backend that performs an
operation for some shares, paths or negotiated protocol versions and not
others. Those cases were forced onto forbidden, which claims the caller lacks
permission when the backend said the operation does not exist here.
The outbound wire mappings are exhaustive over VFSErrorCode, so each one names
its protocol's own unsupported response.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
listDrivePeers returned every peer holding PeerCapabilityTaildriveSharer,
which is an ACL grant usually given to a whole group or tag, so offline
peers and peers with no reachable peerAPI were included. Bump the
tailscale submodule to the fix and correct the TS doc comments, which
claimed the result was peers exposing a share.
The submodule bump also picks up the removal of the obsolete ssh, fetch
and setExitNodeEnabled wasm bridge APIs, already on the fork's webnet
branch and unused here.
Closes#143
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The union merge driver kept both the pre- and post-rebase versions of the
tsconnect shutdown entry; the older one predates the dead-handle change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The child-process probes, the missing shutdown() in the tsconnect
integration teardown, and the *.probe.ts tsconfig excludes all existed
only to keep the late "Go program has already exited" errors away from
node:test. That bug is fixed, so drop the workarounds; the restored
in-process shutdowns are the regression test for it.
Refs #147, #152
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With _resume no longer throwing after exit, a call into a dead Go handle
resolves with undefined instead of failing — a read loop would spin on it
rather than break out. Conn, PacketConn and TCPListener now report closed
once their runtime has exited, so those calls raise ClosedError and
close() is a no-op.
Refs #147
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wasm_exec.js keeps the Go scheduler's setTimeout armed and every js.FuncOf
callback registered across program exit, and _resume throws "Go program has
already exited" when they fire. After a clean shutdown() that surfaces as
uncaught errors and unhandled rejections seconds later: a scheduler timeout,
DERP WebSocket close listeners, and a fetch continuation.
initIPN now clears the scheduled timeouts from runtime.wasmExit and makes
_resume a no-op once the runtime has exited.
Fixes#147
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TSCONNECT_TEST_USER_AUTH_KEY and TSCONNECT_TEST_EXIT_NODE_ID gate the Taildrop
and exit node tests; both are optional and the suite is green without them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>