Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/Config/Filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use CodeIgniter\Filters\InvalidChars;
use CodeIgniter\Filters\PageCache;
use CodeIgniter\Filters\PerformanceMetrics;
use CodeIgniter\Filters\RequestId;
use CodeIgniter\Filters\SecureHeaders;

class Filters extends BaseFilters
Expand All @@ -34,6 +35,7 @@ class Filters extends BaseFilters
'forcehttps' => ForceHTTPS::class,
'pagecache' => PageCache::class,
'performance' => PerformanceMetrics::class,
'requestid' => RequestId::class,
];

/**
Expand All @@ -51,11 +53,13 @@ class Filters extends BaseFilters
*/
public array $required = [
'before' => [
// 'requestid', // Request ID for each request
Comment thread
patel-vansh marked this conversation as resolved.
'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
],
Expand Down
4 changes: 4 additions & 0 deletions system/Config/Filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use CodeIgniter\Filters\InvalidChars;
use CodeIgniter\Filters\PageCache;
use CodeIgniter\Filters\PerformanceMetrics;
use CodeIgniter\Filters\RequestId;
use CodeIgniter\Filters\SecureHeaders;

/**
Expand All @@ -47,6 +48,7 @@ class Filters extends BaseConfig
'forcehttps' => ForceHTTPS::class,
'pagecache' => PageCache::class,
'performance' => PerformanceMetrics::class,
'requestid' => RequestId::class,
];

/**
Expand All @@ -64,11 +66,13 @@ class Filters extends BaseConfig
*/
public array $required = [
'before' => [
// 'requestid', // Request ID for each request
Comment thread
patel-vansh marked this conversation as resolved.
'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
],
Expand Down
66 changes: 66 additions & 0 deletions system/Filters/RequestId.php
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);
Comment thread
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;
}
Comment thread
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;
}
}
169 changes: 169 additions & 0 deletions tests/system/Filters/RequestIdTest.php
Comment thread
patel-vansh marked this conversation as resolved.
Comment thread
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');
Comment thread
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@!#$';
Comment thread
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'));
}
}
1 change: 1 addition & 0 deletions user_guide_src/source/changelogs/v4.8.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ Others
- **Float and Double Casting:** Added support for precision and rounding mode when casting to float or double in entities.
- Added ``CodeIgniter\Input\InputData`` and ``InputDataFactory`` for reusable typed input data objects.
- Float and Double casting now throws ``CastException::forInvalidFloatRoundingMode()`` if an rounding mode other than up, down, even or odd is provided.
- **Filters:** Added ``RequestId`` filter for request tracing and correlation logging. The filter stores the request ID in the request context and automatically adds the ``X-Request-ID`` response header. Incoming ``X-Request-ID`` headers are used when valid. See :ref:`requestid` for details.
- **Environment:** Added ``CodeIgniter\EnvironmentDetector`` class and corresponding ``environment`` service as a mockable wrapper around the ``ENVIRONMENT`` constant.
Framework internals that previously compared ``ENVIRONMENT`` directly now go through this service, making environment-specific branches reachable in tests via ``Services::injectMock()``. See :ref:`environment-detector-service`.
- **Time:** Added ``Time::between()``, ``Time::min()``, and ``Time::max()`` comparison helpers. See :ref:`between <time-comparing-two-times-between>`, :ref:`min <time-comparing-two-times-min>`, and :ref:`max <time-comparing-two-times-max>`.
Expand Down
24 changes: 24 additions & 0 deletions user_guide_src/source/incoming/filters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.
25 changes: 25 additions & 0 deletions user_guide_src/source/incoming/filters/014.php
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
],
];

// ...
}
2 changes: 2 additions & 0 deletions user_guide_src/source/installation/upgrade_480.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ and it is recommended that you merge the updated versions with your application:
Config
------

- app/Config/Filters.php
- Added a new filter named ``requestid`` that adds a unique request ID to each request in the application's context.
- app/Config/Mimes.php
- ``Config\Mimes::$mimes`` added a new key ``md`` for Markdown files.
- app/Config/Routing.php
Expand Down
Loading