Skip to content

Commit

Permalink
Merge pull request #5185 from rldhont/x-request-id
Browse files Browse the repository at this point in the history
Add X-Request-Id in request to QGIS Server headers
  • Loading branch information
rldhont authored Jan 16, 2025
2 parents 62f19fe + 2ada8d1 commit fe6dfdf
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 32 deletions.
16 changes: 11 additions & 5 deletions lizmap/modules/lizmap/lib/Request/OGCRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,16 +233,22 @@ private function formatHttpErrorString($parameters, $code)
/**
* Log if the HTTP code is a 4XX or 5XX error code.
*
* @param int $code The HTTP code of the request
* @param int $code The HTTP code of the request
* @param array<string, string> $headers The headers of the response
*/
protected function logRequestIfError($code)
protected function logRequestIfError($code, $headers)
{
if ($code < 400) {
return;
}

$message = 'The HTTP OGC request to QGIS Server ended with an error.';

$xRequestId = $headers['X-Request-Id'] ?? '';
if ($xRequestId !== '') {
$message .= ' The X-Request-Id `'.$xRequestId.'`.';
}

// The master error with MAP parameter
// This user must have an access to QGIS Server logs
$params = $this->parameters();
Expand Down Expand Up @@ -308,14 +314,14 @@ protected function request($post = false, $stream = false)
if ($stream) {
$response = \Lizmap\Request\Proxy::getRemoteDataAsStream($querystring, $options);

$this->logRequestIfError($response->getCode());
$this->logRequestIfError($response->getCode(), $response->getHeaders());

return new OGCResponse($response->getCode(), $response->getMime(), $response->getBodyAsStream());
}

list($data, $mime, $code) = \Lizmap\Request\Proxy::getRemoteData($querystring, $options);
list($data, $mime, $code, $headers) = \Lizmap\Request\Proxy::getRemoteData($querystring, $options);

$this->logRequestIfError($code);
$this->logRequestIfError($code, $headers);

return new OGCResponse($code, $mime, $data);
}
Expand Down
91 changes: 70 additions & 21 deletions lizmap/modules/lizmap/lib/Request/Proxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ protected static function buildHeaders($url, $options)
$options['headers'] = array_merge(
self::userHttpHeader(),
self::$services->wmsServerHeaders,
array('X-Request-Id' => uniqid().'-'.bin2hex(random_bytes(10))),
$options['headers']
);
}
Expand All @@ -322,15 +323,15 @@ protected static function buildHeaders($url, $options)
* @param string $url
* @param array $options
*
* @return array{0: string, 1: string, 2: int} Array containing data (0: string), mime type (1: string) and HTTP code (2: int)
* @return array{0: string, 1: string, 2: int, 3: array} Array containing data (0: string), mime type (1: string), HTTP code (2: int) and headers
*/
protected static function curlProxy($url, $options)
{
$services = self::getServices();
$http_code = null;

$ch = curl_init();
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
Expand Down Expand Up @@ -381,10 +382,29 @@ protected static function curlProxy($url, $options)
curl_setopt($ch, CURLOPT_POSTFIELDS, $options['body']);
}
}

$data = curl_exec($ch);
if (!$data) {
$data = '';
}
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers_str = substr($data, 0, $header_size);
$data = substr($data, $header_size);

$headers_arr = array_filter(explode("\r\n", $headers_str));
$status_message = array_shift($headers_arr);
$headers = array();
foreach ($headers_arr as $value) {
if (false !== ($matches = explode(':', $value, 2))) {
$key = str_replace(
' ',
'-',
ucwords(strtolower(str_replace('-', ' ', $matches[0])))
);
$headers["{$key}"] = trim($matches[1]);
}
}

$info = curl_getinfo($ch);
$mime = $info['content_type'];
$http_code = (int) $info['http_code'];
Expand All @@ -395,14 +415,14 @@ protected static function curlProxy($url, $options)

curl_close($ch);

return array($data, $mime, $http_code);
return array($data, $mime, $http_code, $headers);
}

/**
* @param string $url
* @param array $options
*
* @return array{0: string, 1: string, 2: int} Array containing data (0: string), mime type (1: string) and HTTP code (2: int)
* @return array{0: string, 1: string, 2: int, 3: array} Array containing data (0: string), mime type (1: string), HTTP code (2: int) and headers
*/
protected static function fileProxy($url, $options)
{
Expand Down Expand Up @@ -459,18 +479,30 @@ protected static function fileProxy($url, $options)
$data = '';
}
$mime = 'image/png';
$matches = array();
$http_code = 0;
$headers = array();
// $http_response_header is created by file_get_contents
foreach ($http_response_header as $header) {
if (preg_match('#^Content-Type:\s+([\w/\.+]+)(;\s+charset=(\S+))?#i', $header, $matches)) {
$mime = $matches[1];
if (count($matches) > 3) {
$mime .= '; charset='.$matches[3];
}
} elseif (substr($header, 0, 5) === 'HTTP/') {
if (substr($header, 0, 5) === 'HTTP/') {
list($version, $code, $phrase) = explode(' ', $header, 3) + array('', false, '');
$http_code = (int) $code;

continue;
}
if (false !== ($matches = explode(':', $header, 2))) {
$key = str_replace(
' ',
'-',
ucwords(strtolower(str_replace('-', ' ', $matches[0])))
);
$headers["{$key}"] = trim($matches[1]);
if ($key === 'Content-Type'
&& preg_match('#^Content-Type:\s+([\w/\.+]+)(;\s+charset=(\S+))?#i', $header, $matches)) {
$mime = $matches[1];
if (count($matches) > 3) {
$mime .= '; charset='.$matches[3];
}
}
}
}
// optional debug
Expand All @@ -480,23 +512,36 @@ protected static function fileProxy($url, $options)
\jLog::dump($http_response_header, 'getRemoteData, bad response, response headers', 'error');
}

return array($data, $mime, $http_code);
return array($data, $mime, $http_code, $headers);
}

/**
* Log if the HTTP code is a 4XX or 5XX error code.
*
* @param int $httpCode The HTTP code of the request
* @param string $url The URL of the request, for logging
* @param int $httpCode The HTTP code of the request
* @param string $url The URL of the request, for logging
* @param array<string, string> $headers The headers of the response
*/
protected static function logRequestIfError($httpCode, $url)
protected static function logRequestIfError($httpCode, $url, $headers = array())
{
if ($httpCode < 400) {
return;
}

\jLog::log('An HTTP request ended with an error, please check the main error log. HTTP code '.$httpCode, 'lizmapadmin');
\jLog::log('The HTTP request ended with an error. HTTP code '.$httpCode.''.$url, 'error');
$xRequestId = $headers['X-Request-Id'] ?? '';

$lizmapAdmin = 'An HTTP request ended with an error, please check the main error log.';
$lizmapAdmin .= ' HTTP code '.$httpCode.'.';
$error = 'The HTTP request ended with an error.';
$error .= ' HTTP code '.$httpCode.'.';
if ($xRequestId !== '') {
$lizmapAdmin .= ' The X-Request-Id `'.$xRequestId.'`.';
$error .= ' X-Request-Id `'.$xRequestId.'` → '.$url;
} else {
$error .= ''.$url;
}
\jLog::log($lizmapAdmin, 'lizmapadmin');
\jLog::log($error, 'error');
}

/**
Expand All @@ -516,7 +561,7 @@ protected static function logRequestIfError($httpCode, $url)
* @param string|string[] $method deprecated. the http method.
* it is ignored if $options is an array.
*
* @return array{0: string, 1: string, 2: int} Array containing data (0: string), mime type (1: string) and HTTP code (2: int)
* @return array{0: string, 1: string, 2: int, 3: array} Array containing data (0: string), mime type (1: string), HTTP code (2: int) and headers
*/
public static function getRemoteData($url, $options = null, $debug = null, $method = 'get')
{
Expand All @@ -534,6 +579,7 @@ public static function getRemoteData($url, $options = null, $debug = null, $meth
$content,
'text/json',
200,
array(),
);
}
// All requests are logged
Expand All @@ -544,13 +590,13 @@ public static function getRemoteData($url, $options = null, $debug = null, $meth
if (extension_loaded('curl') && $options['proxyHttpBackend'] != 'php') {
// With curl
$curlRequest = self::curlProxy($url, $options);
self::logRequestIfError($curlRequest[2], $url);
self::logRequestIfError($curlRequest[2], $url, $curlRequest[3]);

return $curlRequest;
}
// With file_get_contents
$request = self::fileProxy($url, $options);
self::logRequestIfError($request[2], $url);
self::logRequestIfError($request[2], $url, $request[3]);

return $request;
}
Expand Down Expand Up @@ -580,6 +626,7 @@ public static function getRemoteDataAsStream($url, $options = null)
return new ProxyResponse(
200,
'text/json',
array('Content-Type' => 'text/json'),
$stream
);
}
Expand Down Expand Up @@ -634,11 +681,13 @@ public static function getRemoteDataAsStream($url, $options = null)
}

$response = $client->send($request, $reqOptions);
self::logRequestIfError($response->getStatusCode(), $url);
$headers = $response->getHeaders();
self::logRequestIfError($response->getStatusCode(), $url, $headers);

return new ProxyResponse(
$response->getStatusCode(),
$response->getHeader('Content-Type')[0],
$headers,
$response->getBody()
);
}
Expand Down
25 changes: 21 additions & 4 deletions lizmap/modules/lizmap/lib/Request/ProxyResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ class ProxyResponse
*/
protected $mime;

/**
* @var array<string, string> the headers of the response
*/
protected $headers;

/**
* @var StreamInterface the response body as a stream
*/
Expand All @@ -34,14 +39,16 @@ class ProxyResponse
/**
* constructor.
*
* @param int $code the HTTP status code of the response
* @param string $mime the MIME type of the response
* @param StreamInterface $body the response body as a string
* @param int $code the HTTP status code of the response
* @param string $mime the MIME type of the response
* @param array<string, string> $headers the headers of the response
* @param StreamInterface $body the response body as a string
*/
public function __construct($code, $mime, $body)
public function __construct($code, $mime, $headers, $body)
{
$this->code = $code;
$this->mime = $mime;
$this->headers = $headers;
$this->body = $body;
}

Expand All @@ -65,6 +72,16 @@ public function getMime()
return $this->mime;
}

/**
* Get the headers of the response.
*
* @return array<string, string>
*/
public function getHeaders()
{
return $this->headers;
}

/**
* Get the response's body as string.
*
Expand Down
6 changes: 5 additions & 1 deletion tests/end2end/cypress/integration/requests-wfs-ghaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -1247,7 +1247,11 @@ describe('Request service', function () {
.then((result) => {
expect(result.code).to.eq(0)
expect(result.stdout).to.contain('An HTTP request ended with an error, please check the main error log.')
expect(result.stdout).to.contain('HTTP code 400')
expect(result.stdout).to.contain('HTTP code 400.')
expect(result.stdout).to.contain('The HTTP OGC request to QGIS Server ended with an error.')
expect(result.stdout).to.contain(
'HTTP code 400 on "REPOSITORY" = \'testsrepository\' & "PROJECT" = \'selection\' & "SERVICE" = \'WFS\' & "REQUEST" = \'getfeature\''
)
clearLizmapAdminLog()
})
clearErrorsLog()
Expand Down
20 changes: 20 additions & 0 deletions tests/units/classes/Request/ProxyResponseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

use PHPUnit\Framework\TestCase;
use Lizmap\Request;

class ProxyResponseTest extends TestCase
{
public function testGetter() : void
{
$response = new Request\ProxyResponse(
200,
'text/json',
array('Content-Type' => 'text/json'),
\GuzzleHttp\Psr7\Utils::streamFor('{}')
);
$this->assertEquals(200, $response->getCode());
$this->assertEquals('text/json', $response->getMime());
$this->assertEquals(array('Content-Type' => 'text/json'), $response->getHeaders());
}
}
6 changes: 5 additions & 1 deletion tests/units/classes/Request/ProxyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,11 @@ public function testBuildHeaders($options, $expectedHeaders, $expectedBody, $exp
$url = 'http://localhost?test=test';
ProxyForTests::setServices((object)array('wmsServerURL' => 'http://localhost', 'wmsServerHeaders' => array()));
list($url, $result) = ProxyForTests::buildHeadersForTests($url, $options);
$this->assertEquals($expectedHeaders, $result['headers']);
foreach ($expectedHeaders as $header => $value) {
$this->assertArrayHasKey($header, $result['headers']);
$this->assertEquals($value, $result['headers'][$header]);
}
$this->assertArrayHasKey('X-Request-Id', $result['headers']);
$this->assertEquals($expectedBody, $result['body']);
if ($expectedUrl) {
$this->assertEquals($expectedUrl, $url);
Expand Down

0 comments on commit fe6dfdf

Please sign in to comment.