Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e9ae153
fix: improve handling of binary responses in GuzzleHttpClientAspect
huangdijia Dec 12, 2025
5bfa99e
fix: 修复 GuzzleHttpClientAspect 中对响应有效负载的处理逻辑
huangdijia Dec 12, 2025
8c7588b
fix: 修复 GuzzleHttpClientAspect 中对流响应的处理逻辑
huangdijia Dec 12, 2025
b187ab5
fix: 修复 GuzzleHttpClientAspect 中对流响应的处理逻辑
huangdijia Dec 12, 2025
a07dd7d
fix: 改进 GuzzleHttpClientAspect 中对二进制响应的处理逻辑
huangdijia Dec 12, 2025
2f539be
fix: 简化 GuzzleHttpClientAspect 中对流响应的处理逻辑
huangdijia Dec 12, 2025
9714791
fix: 修复 GuzzleHttpClientAspect 中对二进制响应的处理逻辑
huangdijia Dec 12, 2025
8f9479b
fix: 修改 GuzzleHttpClientAspect 中对空字符串响应的处理逻辑
huangdijia Dec 12, 2025
91bb46a
fix: 修复 GuzzleHttpClientAspect 中对可回溯流的处理逻辑
huangdijia Dec 12, 2025
84dfa49
Add grpc to textual Content-Type detection
huangdijia Dec 12, 2025
c85d065
Apply suggestions from code review
huangdijia Dec 12, 2025
5449c77
Apply suggestions from code review
huangdijia Dec 12, 2025
528477f
fix: 修复 GuzzleHttpClientAspect 中对 Content-Type 头的处理逻辑
huangdijia Dec 12, 2025
7f918c9
fix: 处理 GuzzleHttpClientAspect 中的空响应情况
huangdijia Dec 12, 2025
02d2b4e
fix: 优化 GuzzleHttpClientAspect 中的响应类型声明
huangdijia Dec 12, 2025
58d28de
fix: 修复 GuzzleHttpClientAspect 中的响应处理逻辑,确保不再接受空响应
huangdijia Dec 12, 2025
7b4ecd9
fix: 修复 GuzzleHttpClientAspect 中的响应内容检查逻辑,确保正确处理空字符串响应
huangdijia Dec 12, 2025
4dbb73f
Apply suggestion from @Copilot
huangdijia Dec 12, 2025
dbf7222
fix: 优化 GuzzleHttpClientAspect 中的响应内容处理,增加响应内容截断逻辑
huangdijia Dec 12, 2025
31b5b74
fix: 优化 GuzzleHttpClientAspect 中的响应内容截断逻辑,改善代码可读性
huangdijia Dec 12, 2025
9493633
fix: 修正 GuzzleHttpClientAspect 中的常量注释格式,改善代码一致性
huangdijia Dec 12, 2025
abf899f
fix: 优化 GuzzleHttpClientAspect 中的响应内容处理逻辑,确保在异常情况下不影响请求流程
huangdijia Dec 12, 2025
62c36f9
fix: 更新 GuzzleHttpClientAspect 中的内容类型匹配逻辑,支持 gRPC 响应类型
huangdijia Dec 12, 2025
a3749ee
fix: 简化 GuzzleHttpClientAspect 中的响应内容类型判断逻辑
huangdijia Dec 12, 2025
01a9c1d
fix: 优化 GuzzleHttpClientAspect 中的响应内容处理逻辑,支持 gRPC 和纯文本响应
huangdijia Dec 12, 2025
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
78 changes: 61 additions & 17 deletions src/sentry/src/Tracing/Aspect/GuzzleHttpClientAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Sentry\State\Scope;
use Sentry\Tracing\SpanContext;
use Sentry\Tracing\SpanStatus;
use Throwable;

use function FriendsOfHyperf\Sentry\trace;

Expand All @@ -31,6 +33,8 @@
*/
class GuzzleHttpClientAspect extends AbstractAspect
{
private const MAX_RESPONSE_BODY_SIZE = 8192; // 8 KB

public array $classes = [
Client::class . '::transfer',
];
Expand Down Expand Up @@ -116,23 +120,9 @@ function (Scope $scope) use ($proceedingJoinPoint, $options, $guzzleConfig, $req
]);

if ($this->feature->isTracingTagEnabled('http.response.body.contents')) {
$isTextual = \preg_match(
'/^(text\/|application\/(json|xml|x-www-form-urlencoded))/i',
$response->getHeaderLine('Content-Type')
) === 1;
$body = $response->getBody();

if ($isTextual && $body->isSeekable()) {
$pos = $body->tell();
$span->setData([
'http.response.body.contents' => \GuzzleHttp\Psr7\Utils::copyToString($body, 8192), // 8KB 上限
]);
$body->seek($pos);
} else {
$span->setData([
'http.response.body.contents' => '[binary omitted]',
]);
}
$span->setData([
'http.response.body.contents' => $this->getResponsePayload($response, $options),
]);
}

$span->setHttpStatus($response->getStatusCode());
Expand All @@ -156,4 +146,58 @@ function (Scope $scope) use ($proceedingJoinPoint, $options, $guzzleConfig, $req
->setOrigin('auto.http.client')
);
}

protected function getResponsePayload(ResponseInterface $response, array $options = []): mixed
{
if (isset($options['stream']) && $options['stream'] === true) {
return '[Streamed Response]';
}

// Determine if the response is textual based on the Content-Type header.
$contentType = $response->getHeaderLine('Content-Type');
$isTextual = $contentType === '' || \preg_match(
'/^(text\/|application\/(json|xml|x-www-form-urlencoded|grpc))/i',
$contentType
) === 1;

// If the response is not textual or the stream is not seekable, we will return a placeholder.
if (! $isTextual) {
return '[Binary Omitted]';
}

$stream = $response->getBody();
$pos = null;

try {
if ($stream->isSeekable()) {
$pos = $stream->tell();
$stream->rewind();
}

$content = \GuzzleHttp\Psr7\Utils::copyToString(
$stream,
self::MAX_RESPONSE_BODY_SIZE + 1 // 多读 1 byte 用来判断是否截断
);

if (strlen($content) > self::MAX_RESPONSE_BODY_SIZE) {
return substr(
$content,
0,
self::MAX_RESPONSE_BODY_SIZE
) . '… [truncated]';
}

return $content === '' ? '[Empty-String Response]' : $content;
} catch (Throwable $e) {
return '[Error Retrieving Response Content]';
} finally {
if ($pos !== null) {
try {
$stream->seek($pos);
} catch (Throwable) {
// ignore: tracing must not break the request flow
}
}
}
}
}
41 changes: 29 additions & 12 deletions src/telescope/src/Aspect/GuzzleHttpClientAspect.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint)

// Add or override the on_stats option to record the request duration.
$onStats = $options['on_stats'] ?? null;
$proceedingJoinPoint->arguments['keys']['options']['on_stats'] = function (TransferStats $stats) use ($onStats) {
$proceedingJoinPoint->arguments['keys']['options']['on_stats'] = function (TransferStats $stats) use ($onStats, $options) {
try {
$request = $stats->getRequest();
$response = $stats->getResponse();
Expand All @@ -75,7 +75,7 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint)
$content['response_status'] = $response->getStatusCode();
$content['response_headers'] = $response->getHeaders();
$content['response_reason'] = $response->getReasonPhrase();
$content['response_payload'] = $this->getResponsePayload($response);
$content['response_payload'] = $this->getResponsePayload($response, $options);
}

Telescope::recordClientRequest(IncomingEntry::make($content));
Expand All @@ -91,9 +91,26 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint)
return $proceedingJoinPoint->process();
}

public function getResponsePayload(ResponseInterface $response)
public function getResponsePayload(ResponseInterface $response, array $options = []): mixed
{
if (isset($options['stream']) && $options['stream'] === true) {
return 'Streamed Response';
}

// Determine if the response is textual based on the Content-Type header.
$contentType = $response->getHeaderLine('Content-Type');
$isTextual = $contentType === '' || \preg_match(
'/^(text\/|application\/(json|xml|x-www-form-urlencoded|grpc))/i',
$contentType
) === 1;

// If the response is not textual, we will return a placeholder.
if (! $isTextual) {
return 'Binary Response';
}

$stream = $response->getBody();

try {
if ($stream->isSeekable()) {
$stream->rewind();
Expand All @@ -102,23 +119,25 @@ public function getResponsePayload(ResponseInterface $response)
$content = $stream->getContents();

if (is_string($content)) {
// Check if the content is within the size limits.
if (! $this->contentWithinLimits($content)) {
return 'Purged By Hyperf Telescope';
}
// Try to decode JSON responses and hide sensitive parameters.
if (
is_array(json_decode($content, true))
&& json_last_error() === JSON_ERROR_NONE
) {
return $this->contentWithinLimits($content) /* @phpstan-ignore-line */
? $this->hideParameters(json_decode($content, true), Telescope::$hiddenResponseParameters)
: 'Purged By Hyperf Telescope';
}
if (Str::startsWith(strtolower($response->getHeaderLine('content-type') ?: ''), 'text/plain')) {
return $this->contentWithinLimits($content) ? $content : 'Purged By Hyperf Telescope'; /* @phpstan-ignore-line */
return $this->hideParameters(json_decode($content, true), Telescope::$hiddenResponseParameters);
}
// Return gRPC responses and plain text responses as is.
if (Str::contains($response->getHeaderLine('content-type'), 'application/grpc') !== false) {
return TelescopeContext::getGrpcResponsePayload() ?: 'Purged By Hyperf Telescope';
}
// Return plain text responses as is.
if (Str::startsWith(strtolower($response->getHeaderLine('content-type') ?: ''), 'text/plain')) {
return $content;
}
}

if (empty($content)) {
Expand All @@ -127,9 +146,7 @@ public function getResponsePayload(ResponseInterface $response)
} catch (Throwable $e) {
return 'Purged By Hyperf Telescope: ' . $e->getMessage();
} finally {
if ($stream->isSeekable()) {
$stream->rewind();
}
$stream->isSeekable() && $stream->rewind();
}

return 'HTML Response';
Expand Down