Add router features: regex escaping, constraints, any(), and group()#41
Merged
Add router features: regex escaping, constraints, any(), and group()#41
Conversation
CPAN testers with older versions of Net::HTTP2::nghttp2 hit failures: - NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE constant missing (undefined sub) - max_header_list_size setting silently ignored in SETTINGS frames - nghttp2 C library assertion crash in submit_data for WebSocket/HTTP2 Fix by enforcing minimum version 0.007 in two places: 1. HTTP2.pm $AVAILABLE check now calls ->VERSION(0.007), so older installs are treated as unavailable. This cascades to PAGI::Server->has_http2 returning false, which also covers the three test files (02-server-config, 03-detection, 09-cli) that use subtest-level has_http2 skip guards. 2. All 10 HTTP/2 test files with BEGIN block skip guards now check Net::HTTP2::nghttp2->VERSION(0.007) so they cleanly skip_all instead of failing on older installations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add notes about extending ->constraints() to accept coderefs that receive ($value, $scope) for rich post-match validation. Cross- reference from feature #2 to the detailed design notes under #9b. This requires feature #8 (pass/fall-through) for proper failure semantics and is deferred to a future release. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents the approved design for three router improvements: - #1: Tokenizer-based _compile_path() with quotemeta() on literals - #2: Inline {id:\d+} and chained ->constraints() syntax - #3: any() multi-method matcher with wildcard and explicit list modes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
15-task TDD plan covering regex escaping, constraints, any() method, POD updates, and final consistency review. Each task follows red-green cycle with commits at every step. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Literal path segments are now escaped with quotemeta(), fixing incorrect
matching of regex metacharacters like dots and brackets. Also adds support
for {name} and {name:pattern} token types (used by constraint feature).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Routes with inline constraints {name:pattern} now properly filter during
dispatch. If a path parameter fails its constraint regex, the route is
skipped and the next matching route is tried.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allows post-registration constraint application via chaining:
$router->get('/posts/:id' => $h)->constraints(id => qr/^\d+$/);
Constraints are merged into the route's constraint list and checked
during dispatch alongside inline {name:pattern} constraints.
Supports wildcard (all methods) and explicit method list:
$router->any('/health' => $handler); # all methods
$router->any('/res' => $handler, method => ['GET','POST']);
Wildcard routes match any HTTP method. Explicit lists produce 405
with correct Allow header for non-matching methods.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unnecessary $self_ref alias; use $self directly in closures - Eliminate double regex match in dispatch loops (match + capture in one) - Fix POD: routes are checked before mounts, not the other way around Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Approved design for route grouping with prefix stack implementation, three forms (callback, router-object, string), constraint storage refactor, and named route conflict detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
14-task TDD plan for implementing route grouping with prefix stack approach, constraint storage refactor, and all three forms (callback, router-object, string). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Chained constraints (from ->constraints() method) are now stored in
_user_constraints separately from inline constraints (from {name:pattern}
syntax). This ensures chained constraints survive route copying when
routes are moved between routers (needed for upcoming group() feature).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add _group_stack to router constructor and make route(), websocket(), and sse() apply accumulated group prefix/middleware from the stack. The stack starts empty so this has zero behavioral change on existing code. This prepares for group() implementation in the next task. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix nested group iteration order — stack must be traversed in reverse so innermost prefix is prepended first, producing correct path composition (e.g., /orgs/:org_id/teams/:team_id/members) and correct middleware ordering (outer before inner before route). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allow passing a PAGI::App::Router instance to group() to re-register its routes with the parent router's prefix and middleware applied. Implements snapshot semantics, named route propagation, and constraint preservation via _include_router(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents as() from accidentally re-namespacing names from a previous group call. Also adds tests for as() with router-object form and for the clearing behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Explains key differences (route storage, path handling, 405 behavior, named routes, middleware, introspection) with code examples showing when to use each and how to combine them. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
$router->mount('/admin' => 'MyApp::Admin') now auto-requires the
package and calls ->to_app as a class method. Consistent with
group()'s string form. Note: ->as() is not available for stringy
mounts since to_app returns a coderef, not a Router object.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Design doc for 6 code fixes + 1 doc fix from the 5-person conceptual review. Covers secure random, double URL-decode, HTTP/2 path decoding, session cookie defaults, CORS warnings, rate limiter cleanup, and SIGHUP doc correction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Detailed TDD implementation plan for 7 fixes from the conceptual review: secure random utility, double URL-decode, HTTP/2 path decoding, session cookie defaults, CORS warning, rate limiter cleanup, and SIGHUP docs correction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The server already URL-decodes $scope->{path}, so _resolve_path() was
decoding a second time. This created a double-decode vulnerability where
%252e%252e in the original request would become .. after two decode passes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Centralizes secure random byte generation that was duplicated in CSRF and Session middleware. Uses /dev/urandom with Crypt::URandom fallback, and dies (instead of silently falling back to rand()) when no secure source is available. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HTTP/2 was using a manual regex for percent-decoding without UTF-8
decode. Now uses URI::Escape::uri_unescape + Encode::decode('UTF-8')
with FB_CROAK fallback, matching the HTTP/1.1 path in HTTP1.pm.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace inline _secure_random_bytes with the shared secure_random_bytes from PAGI::Utils::Random, eliminating the insecure rand() fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SIGHUP documentation incorrectly implied it could be used for code deploys. Updated to clarify it only restarts worker processes (useful for memory reclamation) and that new workers inherit the parent's already-loaded code. Added note directing users to full restart for code deploys. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace inline _secure_random_bytes with the shared secure_random_bytes from PAGI::Utils::Random, eliminating the insecure rand() fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add SameSite=Lax to the default cookie_options to prevent CSRF via cross-site form submissions. Custom cookie_options still override the default. Updated POD to recommend secure => 1 for production HTTPS deployments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wildcard origins with credentials enabled reflects any Origin with Access-Control-Allow-Credentials, allowing any website to make credentialed cross-origin requests. Emit a warning at init time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stale buckets are cleaned up periodically (configurable via cleanup_interval). A max_buckets safety valve evicts the oldest half when exceeded. Prevents unbounded memory growth in long-running servers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The _h2_create_websocket_scope method was still using the old manual regex for percent-decoding, while _h2_create_scope was already fixed. Apply the same uri_unescape + UTF-8 decode with fallback pattern. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Method, path, query_string, and scheme were interpolated into HTML without escaping. Apply the existing _html_escape() helper to all scope values, matching how headers were already handled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
parse_chunked_body accepted arbitrarily large chunk size values, allowing an attacker to claim enormous chunks and stall the connection. Add max_chunk_size (default 10MB) with early rejection returning 413. Also guard against Perl warnings on oversized hex strings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace rand() with PAGI::Utils::Random::secure_random_bytes for the random component of request IDs. Also fix PID field to be consistently 4 hex chars by masking to lower 16 bits. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… writes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
quotemeta, preventing regex metacharacters (.,+,(, etc.) from being interpreted as patterns{name:pattern}syntax in paths (e.g.,/users/{id:\d+}) for compile-time parameter validation->constraints(name => qr/.../)method for adding constraints after route definitionany()method: Register routes matching any HTTP method, or a specific list of methods viamethod => [...]group()method: Flatten routes under a shared prefix with shared middleware — three forms:$router->group('/api' => [$mw] => sub { ... })$router->group('/api' => $other_router)(snapshot semantics)$router->group('/api' => 'MyApp::Routes::Users')(auto-require)as()namespacingTest plan
prove -l t/app-router.t— existing router tests + constraint storage testprove -l t/router-named-routes.t— named route tests unchangedprove -l t/app-router-group.t— 22 subtests covering all group() forms, middleware, nesting, named routes, as(), WS/SSE, error handling, integrationprove -l t/router-middleware.t t/endpoint-router.t t/sse-router-support.t— no regressionsprove -l t/http2/13-sse-detection.t— SSE detection + scope type over H2prove -l t/http2/14-sse-events.t— full send/receive SSE session over H2prove -l t/http2/15-sse-keepalive.t— keepalive comments over H2prove -l t/http2/16-sse-cleanup.t— disconnect and cleanup over H2prove -l t/sse/12-format-helpers.t— SSE formatting helper unit testsprove -l t/— full suite passes (minus known pre-existing failures in t/42-file-response.t, t/app-file.t)🤖 Generated with Claude Code