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
13 changes: 12 additions & 1 deletion .github/workflows/phpspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,22 @@ jobs:
build:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
guzzle: ['^7.3', '^8.0']

name: "phpspec (guzzle ${{ matrix.guzzle }})"

env:
GUZZLE_CONSTRAINT: ${{ matrix.guzzle }}

steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install
- run: composer require "guzzlehttp/guzzle:$GUZZLE_CONSTRAINT" --no-update --no-interaction
- run: composer update --no-interaction --no-progress
- run: vendor/bin/phpspec run
13 changes: 12 additions & 1 deletion .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,22 @@ jobs:
build:
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
guzzle: ['^7.3', '^8.0']

name: "phpunit (guzzle ${{ matrix.guzzle }})"

env:
GUZZLE_CONSTRAINT: ${{ matrix.guzzle }}

steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install
- run: composer require "guzzlehttp/guzzle:$GUZZLE_CONSTRAINT" --no-update --no-interaction
- run: composer update --no-interaction --no-progress
- run: vendor/bin/phpunit ./tests
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased](https://github.com/HubSpot/hubspot-api-php/compare/14.1.0...HEAD)

### Guzzle 8 support

- `guzzlehttp/guzzle` `^8.0` and `guzzlehttp/psr7` `^3.0` are now allowed; Guzzle 7 remains supported.
- Generated clients no longer call the removed `\GuzzleHttp\Utils::jsonEncode()`; they use `json_encode(..., JSON_THROW_ON_ERROR)`.
- Generated clients no longer assume `RequestException::getResponse()` exists (removed in Guzzle 8, where only `ResponseException` subclasses carry a response). Failures without a response now produce an `ApiException` with `null` headers/body instead of a fatal error, on both the sync and async paths.
- Generated clients catch `Psr\Http\Client\NetworkExceptionInterface` instead of `GuzzleHttp\Exception\ConnectException`, so Guzzle 8's `NetworkException` (send/receive errors, HTTP/2 and HTTP/3 failures) is still converted to an `ApiException`.
- `RetryMiddlewareFactory::getRetryFunctionByConnectionErrors()` now matches `Psr\Http\Client\NetworkExceptionInterface` and reads the cURL errno from the exception message, because Guzzle 8 removed `RequestException::getHandlerContext()` and reclassified cURL errors 52, 55 and 56 as `NetworkException` rather than `ConnectException`. As a side effect, cURL errors 55 and 56 are now retried on Guzzle 7 as well, which is what `TRANSIENT_CURL_ERROR_CODES` always documented.
- `apiRequest()` uppercases the `method` option. Guzzle 7 uppercased request methods, Guzzle 8 sends them verbatim.
- The retry decider callbacks accept any PSR-7 `RequestInterface`/`ResponseInterface` instead of only `GuzzleHttp\Psr7\Request`/`Response`.

## [14.1.0](https://github.com/HubSpot/hubspot-api-php/releases/tag/14.1.0) - 2026-05-12

### Retry Middleware
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ The current package requirements are:

PHP >= 8.1

Guzzle 7 and Guzzle 8 are both supported (`guzzlehttp/guzzle: ^7.3 || ^8.0`).

### Sample apps

Please, take a look at our [Sample apps](https://github.com/HubSpot/sample-apps-list)
Expand Down
46 changes: 27 additions & 19 deletions codegen/Automation/Actions/Api/CallbacksApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@

use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\MultipartStream;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\RequestOptions;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use HubSpot\Client\Automation\Actions\ApiException;
Expand Down Expand Up @@ -168,13 +168,16 @@ public function completeWithHttpInfo($callback_id, $callback_completion_request,
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
// Guzzle 8 exposes a response only on ResponseException subclasses.
$errorResponse = method_exists($e, 'getResponse') ? $e->getResponse() : null;

throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
$errorResponse ? $errorResponse->getHeaders() : null,
$errorResponse ? (string) $errorResponse->getBody() : null
);
} catch (ConnectException $e) {
} catch (NetworkExceptionInterface $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
Expand Down Expand Up @@ -250,17 +253,18 @@ function ($response) use ($returnType) {
return [null, $response->getStatusCode(), $response->getHeaders()];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
// Guzzle 8 exposes a response only on ResponseException subclasses.
$response = method_exists($exception, 'getResponse') ? $exception->getResponse() : null;
$statusCode = $response ? $response->getStatusCode() : 0;
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
$response ? $response->getHeaders() : null,
$response ? (string) $response->getBody() : null
);
}
);
Expand Down Expand Up @@ -323,7 +327,7 @@ public function completeRequest($callback_id, $callback_completion_request, stri
if (isset($callback_completion_request)) {
if (stripos($headers['Content-Type'], 'application/json') !== false) {
# if Content-Type contains "application/json", json_encode the body
$httpBody = \GuzzleHttp\Utils::jsonEncode(ObjectSerializer::sanitizeForSerialization($callback_completion_request));
$httpBody = json_encode(ObjectSerializer::sanitizeForSerialization($callback_completion_request), JSON_THROW_ON_ERROR);
} else {
$httpBody = $callback_completion_request;
}
Expand All @@ -344,7 +348,7 @@ public function completeRequest($callback_id, $callback_completion_request, stri

} elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
# if Content-Type contains "application/json", json_encode the form parameters
$httpBody = \GuzzleHttp\Utils::jsonEncode($formParams);
$httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
} else {
// for HTTP post (form)
$httpBody = ObjectSerializer::buildQuery($formParams);
Expand Down Expand Up @@ -415,13 +419,16 @@ public function completeBatchWithHttpInfo($batch_input_callback_completion_batch
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {
// Guzzle 8 exposes a response only on ResponseException subclasses.
$errorResponse = method_exists($e, 'getResponse') ? $e->getResponse() : null;

throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
$e->getResponse() ? $e->getResponse()->getHeaders() : null,
$e->getResponse() ? (string) $e->getResponse()->getBody() : null
$errorResponse ? $errorResponse->getHeaders() : null,
$errorResponse ? (string) $errorResponse->getBody() : null
);
} catch (ConnectException $e) {
} catch (NetworkExceptionInterface $e) {
throw new ApiException(
"[{$e->getCode()}] {$e->getMessage()}",
(int) $e->getCode(),
Expand Down Expand Up @@ -495,17 +502,18 @@ function ($response) use ($returnType) {
return [null, $response->getStatusCode(), $response->getHeaders()];
},
function ($exception) {
$response = $exception->getResponse();
$statusCode = $response->getStatusCode();
// Guzzle 8 exposes a response only on ResponseException subclasses.
$response = method_exists($exception, 'getResponse') ? $exception->getResponse() : null;
$statusCode = $response ? $response->getStatusCode() : 0;
throw new ApiException(
sprintf(
'[%d] Error connecting to the API (%s)',
$statusCode,
$exception->getRequest()->getUri()
),
$statusCode,
$response->getHeaders(),
(string) $response->getBody()
$response ? $response->getHeaders() : null,
$response ? (string) $response->getBody() : null
);
}
);
Expand Down Expand Up @@ -552,7 +560,7 @@ public function completeBatchRequest($batch_input_callback_completion_batch_requ
if (isset($batch_input_callback_completion_batch_request)) {
if (stripos($headers['Content-Type'], 'application/json') !== false) {
# if Content-Type contains "application/json", json_encode the body
$httpBody = \GuzzleHttp\Utils::jsonEncode(ObjectSerializer::sanitizeForSerialization($batch_input_callback_completion_batch_request));
$httpBody = json_encode(ObjectSerializer::sanitizeForSerialization($batch_input_callback_completion_batch_request), JSON_THROW_ON_ERROR);
} else {
$httpBody = $batch_input_callback_completion_batch_request;
}
Expand All @@ -573,7 +581,7 @@ public function completeBatchRequest($batch_input_callback_completion_batch_requ

} elseif (stripos($headers['Content-Type'], 'application/json') !== false) {
# if Content-Type contains "application/json", json_encode the form parameters
$httpBody = \GuzzleHttp\Utils::jsonEncode($formParams);
$httpBody = json_encode($formParams, JSON_THROW_ON_ERROR);
} else {
// for HTTP post (form)
$httpBody = ObjectSerializer::buildQuery($formParams);
Expand Down
Loading