Skip to content

Behavioral test fixes and error-handling consistency across GLVs#3519

Open
Cole-Greer wants to merge 10 commits into
masterfrom
GLVBehaviouralAlignment
Open

Behavioral test fixes and error-handling consistency across GLVs#3519
Cole-Greer wants to merge 10 commits into
masterfrom
GLVBehaviouralAlignment

Conversation

@Cole-Greer

Copy link
Copy Markdown
Contributor

This PR is a followup to the audit findings from #3436.

Improves error-handling consistency for adversarial HTTP scenarios (empty responses, malformed responses, dead connections) across the Gremlin drivers, and reconciles a few timeout-related test gaps against master's connection-options standardization work.

Java (gremlin-driver)

  • Problem: An empty HTTP response body surfaced as a bare EOFException with no context.
  • Fix: GraphBinaryStreamResponseReader now detects this case and raises a clear "Server returned an empty response body" error.

Go (gremlin-go)

  • Problem: Same empty-body issue as Java — the driver silently returned an empty result set instead of an error.

  • Fix: Empty response bodies now produce a clear error.

  • Problem: TestShouldTimeoutWhenServerNeverResponds was skipped outright since the driver had no client-side read/request timeout at the time.

  • Fix: Un-skipped, now uses the standardized ReadTimeout option (confirmed it correctly bounds this case, since Go's per-read deadline is armed before the first read).

Python (gremlin_python)

  • Problem: Transport-level failures (server closes connection, partial response, empty body) leaked raw aiohttp exception types (ClientConnectionError, ClientPayloadError, ServerDisconnectedError) with no Gremlin-level context.

  • Fix: Wraps these in a new GremlinConnectionError with an actionable message.

  • Problem: A half-closed connection wasn't evicted from the pool after an empty response, so the client could keep reusing a dead connection.

  • Fix: Releases the response on error so the connection is evicted and the pool recovers.

.NET (Gremlin.Net)

  • Problem: A malformed response yielded a non-deterministic exception type (IOException or KeyNotFoundException depending on where deserialization failed), forcing the test to accept either type.
  • Fix: Deserialization failures are now wrapped consistently. Reuses the existing ResponseException (via a new constructor overload with a NoStatusCode sentinel) rather than introducing a separate exception type, keeping one exception type per driver for response-related failures.

JavaScript (gremlin-javascript)

  • Problem: No client-side timeout existed for a server that never responds at all.
  • Fix: Investigated using the newly standardized readTimeoutMillis option, but confirmed it only maps to undici's bodyTimeout, which doesn't start ticking until response parsing begins — so it never fires for a server that sends nothing. The test is left skipped, but with updated context.

VOTE +1

- Add a RequestTimeout option wired to http.Transport.ResponseHeaderTimeout so a
  server that accepts the connection but never responds surfaces a timeout
  instead of hanging (tinkerpop-y32).
- Treat an empty HTTP response body as an error instead of returning an empty
  result set (tinkerpop-zar).
- Unskip and tighten the corresponding behavioral tests.
- Add a requestTimeout option bounding time-to-first-byte (cleared once response
  headers arrive so slow streams are unaffected); reject with a ResponseError
  when the server never responds (tinkerpop-uvl).
- Wrap low-level fetch/transport errors in a Gremlin-aware ResponseError with an
  actionable message, preserving the original error as the cause (tinkerpop-1fn).
- Tighten behavioral test assertions.
- Release the aiohttp response on error so a half-closed connection is evicted
  and the same client recovers after an empty response body (tinkerpop-sti).
- Wrap low-level aiohttp transport errors in GremlinConnectionError with an
  actionable message on both the request and response paths (tinkerpop-1fn).
- Use a distinct "Server returned an empty response body" message for the
  empty-body case (tinkerpop-7s3).
- Tighten behavioral test assertions.
- Add ResponseDeserializationException and wrap response-deserialization
  failures so a malformed response yields a single consistent exception type
  instead of a non-deterministic IOException/KeyNotFoundException
  (tinkerpop-9t5).
- Leave transport-level HttpIOException (premature connection close) unwrapped
  so the partial-content-close path keeps its transport-error semantics.
- Update behavioral test assertions.
…driver)

- Produce a clear "Server returned an empty response body" error instead of a
  bare EOFException when the response body is empty (tinkerpop-28f).
- Add hermetic HttpGremlinRequestEncoder tests validating the User-Agent header
  (present/absent) and that per-request settings (timeout, batchSize,
  materializeProperties) survive encoding into the outgoing request
  (tinkerpop-3tw.8).
Replace the newly-added ResponseDeserializationException with a second
ResponseException constructor, keeping a single exception type for
driver-level response failures (server-reported status errors and
local deserialization failures) and matching the reuse-over-new-type
approach taken in gremlin-javascript for the analogous case.
The requestTimeout option added to gremlin-go and gremlin-javascript in
this branch (a time-to-first-byte / whole-request deadline) duplicates
and conflicts with the readTimeout / readTimeoutMillis option that
master's cross-GLV connection-options standardization already
established as the canonical timeout knob. readTimeout is streaming-safe
(armed per read/chunk) and already covers the server-never-responds
scenario, since the very first read of the response never succeeds.

- gremlin-go: remove connectionSettings.requestTimeout and
  ClientSettings.RequestTimeout; drop the http.Transport.ResponseHeaderTimeout
  wiring. TestShouldTimeoutWhenServerNeverResponds now sets ReadTimeout.
- gremlin-javascript: remove ConnectionOptions.requestTimeout and the
  AbortController-based timeout wrapping in #makeHttpRequest, keeping the
  transport-error-wrapping behavior. The behavioral test now configures
  readTimeoutMillis and asserts on the generic connection-closed message.
This test was written against APIs that no longer exist on the current
gremlin-driver main sources (org.apache.tinkerpop.gremlin.driver.interceptor.
PayloadSerializingInterceptor, HttpGremlinRequestEncoder's pre-bulkResults
constructor, RequestOptions.Builder.timeout()/getTimeout(), and
Tokens.TIMEOUT_MS), all superseded by master's connection-options
standardization work. It was blocking test-compile on gremlin-driver.
Drop the ResponseError wrapping around fetch/response failures in
Connection#makeHttpRequest and #handleResponse, reverting to raw
transport errors bubbling up unwrapped.

Skip should timeout when server never responds: readTimeoutMillis
maps only to undici's bodyTimeout, which doesn't start ticking until
response parsing begins, so it never fires for a server that sends
nothing at all.
- ClientBehaviorIntegrateTest.shouldHandleEmptyResponseBody: the empty-response
  wrapping added an extra RuntimeException layer, shifting the cause chain by
  one. Use ExceptionHelper.getRootCause() instead of asserting a fixed depth,
  and check for the new message text.
- gremlin-python: narrow _TRANSPORT_ERRORS from ClientConnectionError to
  ClientOSError. ClientConnectionError structurally also covers
  ServerTimeoutError/SocketTimeoutError, so a connect/write-phase timeout was
  being wrapped as GremlinConnectionError instead of surfacing as a timeout.
  ClientOSError excludes the timeout branch by definition rather than via an
  added special case.
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