-
Notifications
You must be signed in to change notification settings - Fork 2k
feat: Add RequestId filter #10274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 4.8
Are you sure you want to change the base?
feat: Add RequestId filter #10274
Changes from all commits
eba6d71
144733b
4048c15
6cb3d2c
addeab1
bc99621
aca9c04
08b27f3
cbc2efa
d667168
30634bb
9374144
7445bd4
528d47d
47f6e26
8364767
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * This file is part of CodeIgniter 4 framework. | ||
| * | ||
| * (c) CodeIgniter Foundation <admin@codeigniter.com> | ||
| * | ||
| * For the full copyright and license information, please view | ||
| * the LICENSE file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace CodeIgniter\Filters; | ||
|
|
||
| use CodeIgniter\HTTP\RequestInterface; | ||
| use CodeIgniter\HTTP\ResponseInterface; | ||
|
|
||
| class RequestId implements FilterInterface | ||
| { | ||
| /** | ||
| * Generates a unique request ID for each incoming request and adds it to the request context. | ||
| * If the incoming request already has X-Request-ID header, then that header is used instead. | ||
| * | ||
| * {@inheritDoc} | ||
| */ | ||
| public function before(RequestInterface $request, $arguments = null): ?ResponseInterface | ||
| { | ||
| $requestId = trim($request->getHeaderLine('X-Request-ID')); | ||
|
|
||
| if (! $this->isValidRequestId($requestId)) { | ||
| $requestId = bin2hex(random_bytes(16)); | ||
| } | ||
|
|
||
| context()->set('request_id', $requestId); | ||
|
patel-vansh marked this conversation as resolved.
|
||
|
|
||
| $request->setHeader('X-Request-ID', $requestId); | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * {@inheritDoc} | ||
| */ | ||
| public function after(RequestInterface $request, ResponseInterface $response, $arguments = null): ?ResponseInterface | ||
| { | ||
| if (context()->has('request_id')) { | ||
| $response->setHeader('X-Request-ID', context()->get('request_id')); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
patel-vansh marked this conversation as resolved.
|
||
|
|
||
| private function isValidRequestId(string $requestId): bool | ||
| { | ||
| if ($requestId === '') { | ||
| return false; | ||
| } | ||
|
|
||
| if (strlen($requestId) > 64) { | ||
| return false; | ||
| } | ||
|
|
||
| return preg_match('/^[A-Za-z0-9._:-]+$/', $requestId) === 1; | ||
| } | ||
| } | ||
|
patel-vansh marked this conversation as resolved.
patel-vansh marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * This file is part of CodeIgniter 4 framework. | ||
| * | ||
| * (c) CodeIgniter Foundation <admin@codeigniter.com> | ||
| * | ||
| * For the full copyright and license information, please view | ||
| * the LICENSE file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace CodeIgniter\Filters; | ||
|
|
||
| use CodeIgniter\Test\CIUnitTestCase; | ||
| use PHPUnit\Framework\Attributes\Group; | ||
|
|
||
| /** | ||
| * @internal | ||
| */ | ||
| #[Group('Others')] | ||
| final class RequestIdTest extends CIUnitTestCase | ||
| { | ||
| protected function setUp(): void | ||
| { | ||
| parent::setUp(); | ||
|
|
||
| context()->clearAll(); | ||
| } | ||
|
|
||
| public function testBefore(): void | ||
| { | ||
| $filter = new RequestId(); | ||
| $request = service('request', null, false); | ||
|
|
||
| $filter->before($request); | ||
|
|
||
| $requestId = context()->get('request_id'); | ||
|
patel-vansh marked this conversation as resolved.
|
||
|
|
||
| $requestIdFromHeader = $request->getHeaderLine('X-Request-ID'); | ||
|
|
||
| $this->assertNotEmpty($requestId); | ||
| $this->assertSame($requestId, $requestIdFromHeader); | ||
| $this->assertSame(32, strlen($requestId)); | ||
| $this->assertMatchesRegularExpression('/^[A-Za-z0-9._:-]+$/', $requestId); | ||
| } | ||
|
|
||
| public function testBeforeWithExistingRequestId(): void | ||
| { | ||
| $filter = new RequestId(); | ||
| $request = service('request', null, false); | ||
|
|
||
| $existingRequestId = 'test-request-id-123'; | ||
| $request->setHeader('X-Request-ID', $existingRequestId); | ||
|
|
||
| $filter->before($request); | ||
|
|
||
| $requestId = context()->get('request_id'); | ||
|
|
||
| $requestIdFromHeader = $request->getHeaderLine('X-Request-ID'); | ||
|
|
||
| $this->assertSame($existingRequestId, $requestId); | ||
| $this->assertSame($existingRequestId, $requestIdFromHeader); | ||
| $this->assertMatchesRegularExpression('/^[A-Za-z0-9._:-]+$/', $requestId); | ||
| } | ||
|
|
||
| public function testBeforeWithExistingInvalidRequestId(): void | ||
| { | ||
| $filter = new RequestId(); | ||
| $request = service('request', null, false); | ||
|
|
||
| $existingRequestId = 'Abc@!#$'; | ||
|
patel-vansh marked this conversation as resolved.
|
||
| $request->setHeader('X-Request-ID', $existingRequestId); | ||
|
|
||
| $filter->before($request); | ||
|
|
||
| $requestId = context()->get('request_id'); | ||
|
|
||
| $requestIdFromHeader = $request->getHeaderLine('X-Request-ID'); | ||
|
|
||
| $this->assertNotSame($existingRequestId, $requestId); | ||
| $this->assertSame($requestId, $requestIdFromHeader); | ||
| $this->assertSame(32, strlen($requestId)); | ||
| $this->assertMatchesRegularExpression('/^[A-Za-z0-9._:-]+$/', $requestId); | ||
| } | ||
|
|
||
| public function testBeforeWithExistingLongRequestId(): void | ||
| { | ||
| $filter = new RequestId(); | ||
| $request = service('request', null, false); | ||
|
|
||
| $existingRequestId = str_repeat('a', 65); | ||
| $request->setHeader('X-Request-ID', $existingRequestId); | ||
|
|
||
| $filter->before($request); | ||
|
|
||
| $requestId = context()->get('request_id'); | ||
|
|
||
| $requestIdFromHeader = $request->getHeaderLine('X-Request-ID'); | ||
|
|
||
| $this->assertNotSame($existingRequestId, $requestId); | ||
| $this->assertSame($requestId, $requestIdFromHeader); | ||
| $this->assertSame(32, strlen($requestId)); | ||
| $this->assertMatchesRegularExpression('/^[A-Za-z0-9._:-]+$/', $requestId); | ||
| } | ||
|
|
||
| public function testAfter(): void | ||
| { | ||
| $filter = new RequestId(); | ||
| $request = service('request', null, false); | ||
| $response = service('response', null, false); | ||
|
|
||
| context()->set('request_id', 'test-request-id-123'); | ||
|
|
||
| $filter->after($request, $response); | ||
|
|
||
| $this->assertTrue($response->hasHeader('X-Request-ID')); | ||
| $this->assertSame('test-request-id-123', $response->getHeaderLine('X-Request-ID')); | ||
| } | ||
|
|
||
| public function testAfterWithoutRequestId(): void | ||
| { | ||
| $filter = new RequestId(); | ||
| $request = service('request', null, false); | ||
| $response = service('response', null, false); | ||
|
|
||
| context()->remove('request_id'); | ||
|
|
||
| $filter->after($request, $response); | ||
|
|
||
| $this->assertFalse($response->hasHeader('X-Request-ID')); | ||
| } | ||
|
|
||
| public function testResponseOutputsRequestIdFromRequestHeader(): void | ||
| { | ||
| $filter = new RequestId(); | ||
| $request = service('request', null, false); | ||
| $response = service('response', null, false); | ||
|
|
||
| $existingRequestId = 'test-request-id-123'; | ||
| $request->setHeader('X-Request-ID', $existingRequestId); | ||
|
|
||
| $filter->before($request); | ||
|
|
||
| $filter->after($request, $response); | ||
|
|
||
| $this->assertTrue($response->hasHeader('X-Request-ID')); | ||
| $this->assertSame($existingRequestId, $response->getHeaderLine('X-Request-ID')); | ||
| } | ||
|
|
||
| public function testResponseOutputsGeneratedRequestId(): void | ||
| { | ||
| $filter = new RequestId(); | ||
| $request = service('request', null, false); | ||
| $response = service('response', null, false); | ||
|
|
||
| context()->remove('request_id'); | ||
|
|
||
| $filter->before($request); | ||
|
|
||
| $generatedRequestId = context()->get('request_id'); | ||
|
|
||
| $filter->after($request, $response); | ||
|
|
||
| $this->assertTrue($response->hasHeader('X-Request-ID')); | ||
| $this->assertSame($generatedRequestId, $response->getHeaderLine('X-Request-ID')); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -315,6 +315,7 @@ The filters bundled with CodeIgniter4 are: | |
| - ``forcehttps`` => :ref:`forcehttps` | ||
| - ``pagecache`` => :doc:`PageCache <../general/caching>` | ||
| - ``performance`` => :ref:`performancemetrics` | ||
| - ``requestid`` => :ref:`requestid` | ||
|
|
||
| .. note:: The filters are executed in the order defined in the config file. However, if enabled, ``DebugToolbar`` is always executed last because it should be able to capture everything that happens in the other filters. | ||
|
|
||
|
|
@@ -377,3 +378,26 @@ If you want to customize the headers, extend ``CodeIgniter\Filters\SecureHeaders | |
| .. literalinclude:: filters/011.php | ||
|
|
||
| If you want to know about secure headers, see `OWASP Secure Headers Project <https://owasp.org/www-project-secure-headers/>`_. | ||
|
|
||
| .. _requestid: | ||
|
|
||
| Request ID | ||
| ========== | ||
|
|
||
| .. versionadded:: 4.8.0 | ||
|
|
||
| This filter adds a request ID to each request in context of the application. This can be useful for | ||
| debugging and logging purposes, as it allows you to trace a specific request through the application. | ||
|
|
||
| Framework-generated IDs are 32-character hexadecimal strings and are unique for practical purposes, however valid incoming IDs are reused. | ||
| It is added to the request's context and can be accessed via the ``request_id`` key. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since Context is still a relatively new feature in CodeIgniter, I wonder if it would be helpful to include a small code example showing how to access the request ID from the context. Something along the lines of: $requestId = service('context')->get('request_id');This might make the feature easier to discover for users who are not yet familiar with the Context API. |
||
|
|
||
| To enable this filter, simply add/uncomment the ``requestid`` alias in the ``$required['before']`` and ``$required['after']`` array in **app/Config/Filters.php**: | ||
|
|
||
| .. literalinclude:: filters/014.php | ||
|
|
||
| .. note:: If the incoming request has a header named ``X-Request-ID``, the value of that header | ||
| will be used as the request ID instead of generating a new one or checking for uniqueness. | ||
| The framework does basic validation to ensure that the incoming request ID is a non-empty string, | ||
| has at most 64 characters, and contains only valid characters (alphanumeric, dot, underscore, colon, and hyphen). | ||
| If the validation fails, a new request ID will be generated as normal. This allows you to pass in your own request ID if you have one available, such as from a client or a load balancer. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| <?php | ||
|
|
||
| namespace Config; | ||
|
|
||
| use CodeIgniter\Config\BaseConfig; | ||
|
|
||
| class Filters extends BaseConfig | ||
| { | ||
| // ... | ||
| public array $required = [ | ||
| 'before' => [ | ||
| 'requestid', // Request ID for each request | ||
| 'forcehttps', // Force Global Secure Requests | ||
| 'pagecache', // Web Page Caching | ||
| ], | ||
| 'after' => [ | ||
| 'pagecache', // Web Page Caching | ||
| 'requestid', // Request ID for each request | ||
| 'performance', // Performance Metrics | ||
| 'toolbar', // Debug Toolbar | ||
| ], | ||
| ]; | ||
|
|
||
| // ... | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.