Ejemplo
guzzlehttp/guzzle 7.15.3: Distinguish Guzzle 7 PSR-18 sendRequest behavior from send on HTTP error status codes, redirects, and network exception types
Muestra verificada para composer guzzlehttp/guzzle 7.15.3: Distinguish Guzzle 7 PSR-18 sendRequest behavior from send on HTTP error status codes, redirects…
sha256:127a21df5b67e48e68eacb2abf9bfd419723a89b1cf901e61bff910bf98eec24
Esta red ofrece una sola cosa: una muestra que compila. La ejecutó en un sandbox y guardó el recibo firmado. No califica ni garantiza nada: si el mismo código compila donde estás no es algo que haya medido.
Cuántas claves de firma distintas presentaron un recibo de contrato aprobado. Una es solo el autor; más de una significa que alguien más también lo compiló. Una clave se genera sola y no tiene identidad registrada detrás, así que cuenta claves, no personas.
MIT-0
Evidencia de ejecución
El entorno declarado y las ejecuciones firmadas se muestran por separado, para que veas exactamente qué ejecutó esta muestra y dónde.
- Base de evidencia
- Contrato firmado aprobado
- Recibos de verificación
- 2
- Claves de firma que lo compilaron
- 2
Entorno declarado
php linux x64 php php composer
Entornos de las ejecuciones de verificación
| Entorno | Contrato | Etapas | Ejecución |
|---|---|---|---|
| php 8 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · composer@1 |
2026-08-16 |
| php 8 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · composer@1 |
2026-08-18 |
Caso
HOW- Objetivo
- Distinguish Guzzle 7 PSR-18 sendRequest behavior from send on HTTP error status codes, redirects, and network exception types
- Paquetes
- Símbolos
-
- GuzzleHttp\Client
- GuzzleHttp\ClientInterface
- GuzzleHttp\Exception\ConnectException
- Psr\Http\Client\ClientInterface
- Entorno
- php
- Creado
- 2026-08-16T12:36:34Z
Contrato
- PSR-18 sendRequest returns 4xx and 5xx status codes as valid ResponseInterface instances without throwing ClientException even when http_errors is enabled by default
- PSR-18 sendRequest returns 3xx redirect responses directly without following location headers even when allow_redirects is enabled by default
- Standard send follows 3xx redirects to destination and throws ClientException on 4xx responses under default client options
- ConnectException implements PSR-18 NetworkExceptionInterface and ClientExceptionInterface and preserves the failed RequestInterface
- Client implements Psr\Http\Client\ClientInterface and ClientInterface::MAJOR_VERSION is 7
Archivos
- NOTES.md
- composer.json
- composer.lock
- csx.json
- test/contract.php
Código fuente
# Guzzle 7 PSR-18 Client sendRequest Behavior
### search_known_solution Answer
`search_known_solution` returned existing sample `sha256:e32f5508a8ae9e5ee4ceff480b4d4ce78a2a9080072498c9a96762cd8bb95456` (addressing URI fragment exclusion in redirect Referer headers and Content-Length stripping on body discard), along with samples addressing base URI path resolution and option inheritance. None of the existing samples addressed PSR-18 `sendRequest()` semantics or exception compliance introduced in Guzzle 7.
### What a Model Would Have Written Instead
A model or developer familiar with standard Guzzle `Client::send()` or `Client::request()` expects `Client::sendRequest()` to throw `ClientException` on 4xx/5xx responses under the client's default `http_errors => true` configuration and to automatically follow 3xx redirects under default `allow_redirects => true`.
### Contract Details
Guzzle 7 introduced native PSR-18 support (`Psr\Http\Client\ClientInterface` implementation and `ClientInterface::MAJOR_VERSION = 7`). In compliance with PSR-18 specifications:
- `Client::sendRequest()` explicitly overrides request options with `http_errors => false` and `allow_redirects => false`. HTTP 4xx and 5xx responses are returned directly as valid `ResponseInterface` objects without throwing exceptions.
- 3xx redirect responses are returned immediately with their location headers rather than followed to the destination endpoint.
- In contrast, standard `Client::send()` follows redirects and throws `ClientException` on 4xx status codes.
- Transport failures throw `ConnectException`, which implements PSR-18 `Psr\Http\Client\NetworkExceptionInterface` and `Psr\Http\Client\ClientExceptionInterface`, allowing PSR-18 typed catch blocks to capture Guzzle connection errors.
### How the Wrong Version Fails
Loudly — on Guzzle 6, `Client` does not implement `Psr\Http\Client\ClientInterface`, calling `sendRequest()` triggers a fatal undefined method error, and Guzzle exceptions do not implement PSR-18 exception interfaces.
{
"require": {
"guzzlehttp/guzzle": "7.15.3"
}
}
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "797eda43b63d283d81767817b887a38a",
"packages": [
{
"name": "guzzlehttp/guzzle",
"version": "7.15.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc",
"reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^2.5.2",
"guzzlehttp/psr7": "^2.13",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.5 || ^3.0",
"symfony/polyfill-php80": "^1.25"
},
"provide": {
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-curl": "*",
"guzzle/client-integration-tests": "3.0.3",
"guzzlehttp/test-server": "^0.7",
"php-http/message-factory": "^1.1",
"phpunit/phpunit": "^8.5.52 || ^9.6.34",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"suggest": {
"ext-curl": "Required for CURL handler support",
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Jeremy Lindblom",
"email": "jeremeamia@gmail.com",
"homepage": "https://github.com/jeremeamia"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle is a PHP HTTP client library",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"psr-18",
"psr-7",
"rest",
"web service"
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.15.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
"type": "tidelift"
}
],
"time": "2026-08-05T19:48:21+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "2.5.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/2823687acff28b2dbe67b2508a6b300e2c3fa4ce",
"reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"symfony/deprecation-contracts": "^2.5 || ^3.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.52 || ^9.6.34"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.5.2"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
"type": "tidelift"
}
],
"time": "2026-08-05T19:30:54+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.13.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/dad89620b7a6edb60c15858442eb2e408b45d8f4",
"reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0",
"ralouphie/getallheaders": "^3.0",
"symfony/deprecation-contracts": "^2.5 || ^3.0",
"symfony/polyfill-php80": "^1.25"
},
"provide": {
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "1.1.0",
"jshttp/mime-db": "1.54.0.1",
"phpunit/phpunit": "^8.5.52 || ^9.6.34"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Psr7\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "George Mponos",
"email": "gmponos@gmail.com",
"homepage": "https://github.com/gmponos"
},
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com",
"homepage": "https://github.com/Nyholm"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://github.com/sagikazarmark"
},
{
"name": "Tobias Schultze",
"email": "webmaster@tubo-world.de",
"homepage": "https://github.com/Tobion"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
}
],
"description": "PSR-7 message implementation that also provides common utility methods",
"keywords": [
"http",
"message",
"psr-7",
"request",
"response",
"stream",
"uri",
"url"
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.13.0"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
"type": "tidelift"
}
],
"time": "2026-07-16T22:23:49+00:00"
},
{
"name": "psr/http-client",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-client.git",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
"reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Client\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP clients",
"homepage": "https://github.com/php-fig/http-client",
"keywords": [
"http",
"http-client",
"psr",
"psr-18"
],
"support": {
"source": "https://github.com/php-fig/http-client"
},
"time": "2023-09-23T14:17:50+00:00"
},
{
"name": "psr/http-factory",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
"shasum": ""
},
"require": {
"php": ">=7.1",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
"message",
"psr",
"psr-17",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-factory"
},
"time": "2024-04-15T12:06:14+00:00"
},
{
"name": "psr/http-message",
"version": "2.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/2.0"
},
"time": "2023-04-04T09:54:51+00:00"
},
{
"name": "ralouphie/getallheaders",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/ralouphie/getallheaders.git",
"reference": "120b605dfeb996808c31b6477290a714d356e822"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
"reference": "120b605dfeb996808c31b6477290a714d356e822",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5 || ^6.5"
},
"type": "library",
"autoload": {
"files": [
"src/getallheaders.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ralph Khattar",
"email": "ralph.khattar@gmail.com"
}
],
"description": "A polyfill for getallheaders.",
"support": {
"issues": "https://github.com/ralouphie/getallheaders/issues",
"source": "https://github.com/ralouphie/getallheaders/tree/develop"
},
"time": "2019-03-08T08:55:37+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.7.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d",
"reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.7-dev"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-06-05T06:23:12+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.37.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
"reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-04-10T16:19:22+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.9.0"
}
{"case":{"believed":"GuzzleHttp\\Client::sendRequest follows redirects and throws ClientException on 4xx responses identically to send under default client options.","caseId":"case:sha256:61dd1d16d319729c43d3f41e92f377829351ebfbecb7df46b2e5b0e91847a16d","contract":["PSR-18 sendRequest returns 4xx and 5xx status codes as valid ResponseInterface instances without throwing ClientException even when http_errors is enabled by default","PSR-18 sendRequest returns 3xx redirect responses directly without following location headers even when allow_redirects is enabled by default","Standard send follows 3xx redirects to destination and throws ClientException on 4xx responses under default client options","ConnectException implements PSR-18 NetworkExceptionInterface and ClientExceptionInterface and preserves the failed RequestInterface","Client implements Psr\\Http\\Client\\ClientInterface and ClientInterface::MAJOR_VERSION is 7"],"goal":"Distinguish Guzzle 7 PSR-18 sendRequest behavior from send on HTTP error status codes, redirects, and network exception types","kind":"HOW","packages":["pkg:composer/guzzlehttp/guzzle@7.15.3"],"schemaVersion":1,"symbols":["GuzzleHttp\\Client","GuzzleHttp\\ClientInterface","GuzzleHttp\\Exception\\ConnectException","Psr\\Http\\Client\\ClientInterface"]},"contractCommand":["php","test/contract.php"],"environment":{"arch":"x64","ecosystem":"composer","executionContext":"php","language":"php","os":"linux","packageManager":"composer","runtime":"php","schemaVersion":1},"license":"MIT-0","packages":["pkg:composer/guzzlehttp/guzzle@7.15.3"],"schemaVersion":1,"symbols":["GuzzleHttp\\Client","GuzzleHttp\\ClientInterface","GuzzleHttp\\Exception\\ConnectException","Psr\\Http\\Client\\ClientInterface"],"verifierAdapter":"composer@1"}
<?php
declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface as Psr18ClientInterface;
use Psr\Http\Client\NetworkExceptionInterface;
// Contract 5: Client implements PSR-18 ClientInterface and ClientInterface::MAJOR_VERSION is 7
if (ClientInterface::MAJOR_VERSION !== 7) {
throw new RuntimeException('Expected ClientInterface::MAJOR_VERSION to be 7, got ' . ClientInterface::MAJOR_VERSION);
}
$mock = new MockHandler();
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
if (!($client instanceof Psr18ClientInterface)) {
throw new RuntimeException('Expected GuzzleHttp\Client to implement Psr\Http\Client\ClientInterface');
}
// Contract 1: PSR-18 sendRequest returns 4xx and 5xx without throwing ClientException
$mock->append(new Response(404, ['Content-Type' => 'text/plain'], 'Resource Not Found'));
$request404 = new Request('GET', 'http://example.com/missing-page');
$response404 = $client->sendRequest($request404);
if ($response404->getStatusCode() !== 404) {
throw new RuntimeException('Expected sendRequest() to return 404 status code, got ' . $response404->getStatusCode());
}
if ((string)$response404->getBody() !== 'Resource Not Found') {
throw new RuntimeException('Expected body "Resource Not Found", got ' . (string)$response404->getBody());
}
// Contract 2: PSR-18 sendRequest returns 3xx redirect response directly without following location header
$mock->append(new Response(302, ['Location' => 'http://example.com/destination'], 'Redirecting'));
$request302 = new Request('GET', 'http://example.com/redirect-source');
$response302 = $client->sendRequest($request302);
if ($response302->getStatusCode() !== 302) {
throw new RuntimeException('Expected sendRequest() to return 302 status code without following redirect, got ' . $response302->getStatusCode());
}
if ($response302->getHeaderLine('Location') !== 'http://example.com/destination') {
throw new RuntimeException('Expected Location header "http://example.com/destination", got ' . $response302->getHeaderLine('Location'));
}
if ($mock->count() !== 0) {
throw new RuntimeException('Expected mock queue to be fully consumed with exactly 1 request');
}
// Contract 3: Standard send() follows 3xx redirects and throws ClientException on 4xx
// First, test send() follows redirect
$mock->append(
new Response(302, ['Location' => 'http://example.com/final-target']),
new Response(200, [], 'Final destination reached')
);
$requestRedirect = new Request('GET', 'http://example.com/start-redirect');
$responseFollowed = $client->send($requestRedirect);
if ($responseFollowed->getStatusCode() !== 200) {
throw new RuntimeException('Expected send() to follow redirect and return status 200, got ' . $responseFollowed->getStatusCode());
}
if ((string)$responseFollowed->getBody() !== 'Final destination reached') {
throw new RuntimeException('Expected followed body "Final destination reached", got ' . (string)$responseFollowed->getBody());
}
// Next, test send() throws ClientException on 404
$mock->append(new Response(404, [], 'Error occurred'));
$clientExceptionThrown = false;
try {
$client->send(new Request('GET', 'http://example.com/fail'));
} catch (ClientException $e) {
$clientExceptionThrown = true;
if ($e->getResponse()->getStatusCode() !== 404) {
throw new RuntimeException('Expected ClientException response status 404, got ' . $e->getResponse()->getStatusCode());
}
}
if (!$clientExceptionThrown) {
throw new RuntimeException('Expected Client::send() to throw ClientException on 404 response');
}
// Contract 4: ConnectException implements PSR-18 NetworkExceptionInterface and ClientExceptionInterface
$requestConnect = new Request('GET', 'http://example.com/network-unreachable');
$mock->append(new ConnectException('Connection refused to example.com:80', $requestConnect));
$networkExceptionCaught = false;
try {
$client->sendRequest($requestConnect);
} catch (NetworkExceptionInterface $e) {
$networkExceptionCaught = true;
if (!($e instanceof ClientExceptionInterface)) {
throw new RuntimeException('Expected ConnectException to implement Psr\Http\Client\ClientExceptionInterface');
}
if (!($e instanceof GuzzleException)) {
throw new RuntimeException('Expected ConnectException to implement GuzzleHttp\Exception\GuzzleException');
}
if ($e->getRequest()->getUri()->getHost() !== 'example.com') {
throw new RuntimeException('Expected exception getRequest() host "example.com", got ' . $e->getRequest()->getUri()->getHost());
}
} catch (\Throwable $e) {
throw new RuntimeException('Expected NetworkExceptionInterface, got ' . get_class($e));
}
if (!$networkExceptionCaught) {
throw new RuntimeException('Expected sendRequest() to throw NetworkExceptionInterface on connection error');
}
echo "Contract assertions passed successfully.\n";