8000 feat: add RequestTimeoutMiddleware by kawasima · Pull Request #146 · enkan/enkan · GitHub
[go: up one dir, main page]

Skip to content

feat: add RequestTimeoutMiddleware - #146

Merged
kawasima merged 5 commits into
developfrom
feature/issue-140-request-timeout
Apr 4, 2026
Merged

kawasima merged 5 commits into
developfrom
feature/issue-140-request-timeout

Conversation

@kawasima
@kawasima kawasima commented Apr 3, 2026 •
Copy link
Copy Markdown
Collaborator

Summary

  • Add RequestTimeoutMiddleware that enforces a per-request deadline using StructuredTaskScope
  • Runs chain.next() in a virtual thread forked by StructuredTaskScope; ScopedValue bindings are inherited
  • Returns 504 Gateway Timeout (configurable via setTimeoutStatus(int)) if the handler exceeds timeoutMillis (default 30 s)
  • Equivalent to Hono's timeout() middleware

Design decisions

Decision Rationale
StructuredTaskScope (not singleton executor) Inherits ScopedValue bindings from the calling thread; structured shutdown ensures subtask cannot outlive the scope
--enable-preview on enkan-web StructuredTaskScope is a preview API in Java 25; all class files in the module are marked preview
504 default status Matches Hono's default; configurable via setTimeoutStatus()
Doma2 note in Javadoc ThreadLocal-based transaction context is not inherited; ordering guidance documented

Test plan

  • Normal request completes before timeout → original status returned
  • Slow handler exceeds timeout → 504 returned
  • Custom status code via setTimeoutStatus(503)
  • Handler exception propagates independently of timeout
  • setTimeoutMillis(0) / negative → MisconfigurationException
  • Invalid status code (99 / 600) → MisconfigurationException
  • InterruptedException propagates as RuntimeException (not swallowed as 504)
  • Null response from handler passes through
  • ScopedValue binding is inherited by the subtask

🤖 Generated with Claude Code

kawasima and others added 4 commits April 4, 2026 08:47
Add a per-request timeout middleware that runs chain.next() in a
virtual thread via a singleton executor. Returns 504 Gateway Timeout
(configurable) if the handler does not complete within the configured
deadline. Equivalent to Hono's timeout() and Express's express-timeout.

StructuredTaskScope was considered but remains a preview API in Java 25,
so a singleton Executors.newVirtualThreadPerTaskExecutor() is used instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Change executor from static final to instance field so that close()
can shut it down. This matches the MemoryStore pattern and enables
graceful shutdown: in-flight virtual threads are allowed to complete
before the executor terminates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the executor-based implementation with StructuredTaskScope,
which provides two improvements:

1. ScopedValue inheritance — fork() propagates scoped-value bindings
   to the subtask thread, unlike executor.submit()
2. Structured shutdown — scope.close() waits for the subtask to finish,
   eliminating the need for a separate Closeable lifecycle

Timeout is configured via Configuration.withTimeout() rather than
Future.get(timeout), making intent explicit. enkan-web is compiled
with --enable-preview; applications must use --enable-preview at
runtime only if they reference this class.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Document scope.close() blocking behavior after timeout in Javadoc
- Validate timeoutStatus range (100-599) with MisconfigurationException
- Add tests: invalid timeoutStatus, null response passthrough

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new web middleware to enforce a per-request processing deadline and introduces tests + build configuration needed to run it.

Changes:

  • Introduce RequestTimeoutMiddleware that returns a configurable timeout response when downstream processing exceeds a deadline.
  • Add JUnit coverage for normal completion, timeout behavior, custom timeout status, exception propagation, null response passthrough, and ScopedValue inheritance.
  • Update enkan-web Maven build to pass --enable-preview to compilation and test execution.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
enkan-web/src/main/java/enkan/middleware/RequestTimeoutMiddleware.java Implements request timeout behavior using StructuredTaskScope, plus configurable timeout settings and response.
enkan-web/src/test/java/enkan/middleware/RequestTimeoutMiddlewareTest.java Adds unit tests validating timeout and non-timeout behaviors (including ScopedValue behavior).
enkan-web/pom.xml Enables Java preview features for compiling/running the module and its tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread enkan-web/pom.xml Outdated
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--enable-preview</argLine>
Copilot AI Apr 4, 2026
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The surefire here replaces (not appends to) the parent argLine (pom.xml:104-108). This drops the existing JVM flags (e.g., --add-opens and -XX:+EnableDynamicAgentLoading), which can cause module-specific test/runtime regressions. Consider appending --enable-preview to the inherited argLine instead of overwriting it.

Suggested change
<argLine>--enable-preview</argLine>
<argLine>${argLine} --enable-preview</argLine>

Copilot uses AI. Check for mistakes.
Comment thread enkan-web/pom.xml
Comment on lines +17 to +29
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
<testCompilerArgs>
<arg>--enable-preview</arg>
</testCompilerArgs>
</configuration>
Copilot AI Apr 4, 2026
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling --enable-preview for the main compilation in this module is a breaking operational change: class files produced by javac with preview enabled require the application JVM to be started with --enable-preview to load them. If the intent is to keep enkan-web usable without preview flags unless RequestTimeoutMiddleware is used, consider avoiding preview APIs (per the PR description) or isolating preview-dependent code into a separate artifact/module.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +23
8000
/**
* Middleware that enforces a per-request timeout for downstream processing,
* using {@link StructuredTaskScope}.
*
* <p>The downstream chain runs in a virtual thread forked by
* {@link StructuredTaskScope}. If the chain does not complete within the
* configured timeout, the scope is cancelled and this middleware returns
* a {@code 504 Gateway Timeout} response (configurable via
* {@link #setTimeoutStatus(int)}).
Copilot AI Apr 4, 2026
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation uses StructuredTaskScope and requires --enable-preview, but the PR description states StructuredTaskScope is not used and that the middleware runs chain.next() via a singleton Executors.newVirtualThreadPerTaskExecutor(). Please align the implementation and PR description/design decision so reviewers/users aren’t misled about the chosen concurrency mechanism and preview requirements.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +41
* Applications that reference this class must also be <em>run</em> with
* {@code --enable-preview}; applications that do not reference it are
* unaffected.
Copilot AI Apr 4, 2026
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Javadoc says only applications that reference this class must be run with --enable-preview, but enabling preview compilation at the module level typically marks generated class files as preview and requires --enable-preview to load them at runtime. If you keep module-wide --enable-preview, this note should be updated to reflect that enkan-web consumers will need the flag even if they don’t use RequestTimeoutMiddleware.

Suggested change
* Applications that reference this class must also be <em>run</em> with
* {@code --enable-preview}; applications that do not reference it are
* unaffected.
* If enkan-web is built with module-wide preview enabled, applications that
* load classes from that build must be <em>run</em> with
* {@code --enable-preview}, even if they do not directly use this middleware.

Copilot uses AI. Check for mistakes.
throw new RuntimeException(cause);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return timeoutResponse();
Copilot AI Apr 4, 2026
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InterruptedException here is treated as a timeout and converted to a 504 response. An interrupt can also indicate shutdown/cancellation unrelated to request deadline, and converting it to 504 can hide the interruption from upstream lifecycle handling. Consider propagating interruption (e.g., rethrow as unchecked after restoring interrupt status) or mapping it to a more appropriate response distinct from timeout.

Suggested change
return timeoutResponse();
throw new RuntimeException("Request processing interrupted", e);

Copilot uses AI. Check for mistakes.
- pom.xml: preserve parent argLine flags (--add-opens, -XX:+EnableDynamic
  AgentLoading) alongside --enable-preview instead of overwriting them
- Javadoc: clarify that all enkan-web consumers need --enable-preview at
  runtime, not only users of RequestTimeoutMiddleware
- InterruptedException: propagate as RuntimeException instead of mapping
  to a 504 timeout response, to avoid hiding shutdown/cancellation signals
- Add test: interruptedExceptionPropagatesAsRuntimeException

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kawasima
kawasima merged commit b9f66f0 into develop Apr 4, 2026
1 check 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.

2 participants

0