Skip to content
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

Improve exceptions by adding request/response context and json_serialize support #32

Merged
merged 2 commits into from
Sep 8, 2023
Merged
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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"require": {
"php": ">=7.4",
"ext-curl": "*",
"ext-json": "*"
"ext-json": "*",
"vanilla/garden-utils": "^1.1"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
Expand Down
8 changes: 1 addition & 7 deletions src/HttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,7 @@ public function get(string $uri, array $query = [], array $headers = [], $option
*/
public function handleErrorResponse(HttpResponse $response, $options = []) {
if ($options['throw'] ?? $this->throwExceptions) {
$body = $response->getBody();
if (is_array($body)) {
$message = $body['message'] ?? $response->getReasonPhrase();
} else {
$message = $response->getReasonPhrase();
}
throw new HttpResponseException($response, $message);
throw $response->asException();
}
}

Expand Down
14 changes: 13 additions & 1 deletion src/HttpRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/**
* Representation of an outgoing, client-side request.
*/
class HttpRequest extends HttpMessage {
class HttpRequest extends HttpMessage implements \JsonSerializable {
/// Constants ///

const METHOD_DELETE = 'DELETE';
Expand Down Expand Up @@ -217,4 +217,16 @@ public function getOptions(): array {
'verifyPeer' => $this->getVerifyPeer()
];
}

/**
* Basic JSON implementation.
*
* @return array
*/
public function jsonSerialize(): array {
return [
"url" => $this->getUrl(),
"method" => $this->getMethod(),
];
}
}
47 changes: 46 additions & 1 deletion src/HttpResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/**
* Representation of an outgoing, server-side response.
*/
class HttpResponse extends HttpMessage implements \ArrayAccess {
class HttpResponse extends HttpMessage implements \ArrayAccess, \JsonSerializable {
/// Properties ///

/**
Expand Down Expand Up @@ -433,4 +433,49 @@ public function setRequest(HttpRequest $request = null) {
$this->request = $request;
return $this;
}

/**
* Convert the response into an exception.
*
* @return HttpResponseException
*/
public function asException(): HttpResponseException {
$request = $this->getRequest();
if ($request !== null) {
$requestID = "Request {$request->getMethod()} {$request->getUrl()}";
} else {
$requestID = "Unknown request";
}

if ($this->isSuccessful()) {
$responseAction = "returned a response code of {$this->getStatusCode()}";
} else {
$responseAction = "failed with a response code of {$this->getStatusCode()}";
}

$body = $this->getBody();
if (is_array($body) && isset($body['message']) && is_string($body['message'])) {
$responseMessage = "and a custom message of \"{$body['message']}\"";
} else {
$responseMessage = "and a standard message of \"{$this->getReasonPhrase()}\"";
}

$message = implode(" ", [$requestID, $responseAction, $responseMessage]);

return new HttpResponseException($this, $message);
}

/**
* Basic JSON implementation.
*
* @return array
*/
public function jsonSerialize(): array {
return [
"statusCode" => $this->getStatusCode(),
"content-type" => $this->getHeader("content-type") ?: null,
"request" => $this->getRequest(),
"body" => $this->getRawBody(),
];
}
}
26 changes: 24 additions & 2 deletions src/HttpResponseException.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@

namespace Garden\Http;

use Garden\Utils\ContextException;
use Monolog\Utils;

/**
* An exception that occurs when there is a non 2xx response.
*/
class HttpResponseException extends \Exception {
class HttpResponseException extends ContextException implements \JsonSerializable {
/**
* @var HttpResponse
*/
Expand All @@ -24,7 +27,13 @@ class HttpResponseException extends \Exception {
* @param string $message The error message.
*/
public function __construct(HttpResponse $response, $message = "") {
parent::__construct($message, $response->getStatusCode(), null);
$responseJson = $response->jsonSerialize();
unset($responseJson['request']);
$context = [
"response" => $responseJson,
"request" => $response->getRequest()->jsonSerialize(),
];
parent::__construct($message, $response->getStatusCode(), $context);
$this->response = $response;
}

Expand All @@ -47,4 +56,17 @@ public function getRequest(): HttpRequest {
public function getResponse(): HttpResponse {
return $this->response;
}

/**
* @return array
*/
public function jsonSerialize(): array {
$result = [
'message' => $this->getMessage(),
'status' => (int) $this->getHttpStatusCode(),
'class' => get_class($this),
'code' => (int) $this->getCode(),
] + $this->getContext();
return $result;
}
}
2 changes: 1 addition & 1 deletion tests/HttpMessageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ public function testResponseArrayAccess() {
* Test requesting to an host that doesn't resolve.
*/
public function testUnresolvedUrl() {
$request = new HttpRequest("GET", "http://foo.foo");
$request = new HttpRequest("GET", "http://foo.example");
$request->setTimeout(1);
$response = $request->send();

Expand Down
40 changes: 40 additions & 0 deletions tests/HttpResponseExceptionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
/**
* @copyright 2009-2023 Vanilla Forums Inc.
* @license MIT
*/

namespace Garden\Http\Tests;

use Garden\Http\HttpRequest;
use Garden\Http\HttpResponse;
use PHPUnit\Framework\TestCase;

/**
* Tests for exceptions.
*/
class HttpResponseExceptionTest extends TestCase {

/**
* Test json serialize implementation of exceptions.
*/
public function testJsonSerialize(): void {
$response = new HttpResponse(501, ["content-type" => "application/json"], '{"message":"Some error occured."}');
$response->setRequest(new HttpRequest("POST", "/some/path"));
$this->assertEquals([
"message" => 'Request POST /some/path failed with a response code of 501 and a custom message of "Some error occured."',
"status" => 501,
"code" => 501,
"request" => [
'url' => '/some/path',
'method' => 'POST',
],
"response" => [
'statusCode' => 501,
'content-type' => 'application/json',
'body' => '{"message":"Some error occured."}',
],
'class' => 'Garden\Http\HttpResponseException',
], $response->asException()->jsonSerialize());
}
}