Search code examples
reactjslaravelguzzlepaypal-rest-sdk

Guzzle: Can't catch exception details


I'm mocking negative responses with PayPal API in order to get the responses and handle correctly a critical part of payment when the client has approved the payment via onApprove method.

I'm using GuzzleHttp + Laravel to capture the approval from the client. I get the COMPLETED status within the complete object. So the request is working properly.

$client = new \GuzzleHttp\Client();

return
$response = $client->request(
    'POST',
    'https://api-m.sandbox.paypal.com/v2/checkout/orders/' . $paypalOrderId . '/capture',
    [
        'headers' => [
            'Content-Type' => 'application/json',
            'Authorization' => 'Bearer ' . $access_token,
            // 'PayPal-Mock-Response' => json_encode(["mock_application_codes" => "INTERNAL_SERVER_ERROR"]),
            'PayPal-Mock-Response' => json_encode(["mock_application_codes" => "INSTRUMENT_DECLINED"]),
        ],
    ],
);

I'm mocking the response error via adding a the header => 'PayPal-Mock-Response' => json_encode(["mock_application_codes" => "INSTRUMENT_DECLINED"])which will -of course- break the code if is not inside a try-catch block.

This is the output from Laravel when request has no try-catch:

      "message": "Client error: `POST https://api-m.sandbox.paypal.com/v2/checkout/orders/6JE880117B631364H/capture` resulted in a `422 Unprocessable Entity` 
         response:\n{\n  \"name\": \"UNPROCESSABLE_ENTITY\",\n  \"details\": [\n    {\n      \"issue\": \"INSTRUMENT_DECLINED\",\n      \"description\": \"The (truncated...)\n",
        "exception": "GuzzleHttp\\Exception\\ClientException",
        "file": "C:\\xampp\\htdocs\\react\\React-Laravel\\vinos-gdl\\vendor\\guzzlehttp\\guzzle\\src\\Exception\\RequestException.php",
        "line": 113,

This error is expected. Since I'm forcing it with the Mock-Response header.

The "problem" arises when I insert the code within a try-catch block. I get the expected Exception but I can't get any details from the response:

catch (ServerException $e) {

    if ($e->hasResponse()) {
        return response()->json(['msg' => 'Server Error', 'error' => $e->getResponse()], 500);
    }
    return response()->json([
        'msg' => 'Server Error',
        'request' => $e->getRequest(),
        $e->hasResponse() ? $e->getResponse() : ""
    ]);

    // return response()->json(['msg' => 'Client Error', 'error' => $e->getRequest()]);
} catch (ClientException $e) {

    if ($e->hasResponse()) {
        return response()->json(['msg' => 'Client Error', 'error' => $e->getResponse()], 400);
    }
    return response()->json([
        'msg' => 'Client Error',
        'request' => $e->getRequest(),
        $e->hasResponse() ? $e->getResponse() : ""
    ]);
    // return response()->json(['msg' => 'Server Error', 'error' => report($e)]);
}
catch (BadResponseException $e){
    return response()->json(['error' => $e]);
}

This is the output of a ClientException:

Error: Request failed with status code 400

{
    "msg": "Client Error",
    "error": {} // empty!!!
}

If I force the ServerException:

{
    "msg": "Server Error",
    "error": {} // also empty
}

The Exceptions are thrown, however, this is certainly not enough information to handle correctly the error. I need to get the details from the response.

The front end in case somebody wants to see it:

const onApprove = (data, actions) => {
    console.log("payment approved by user", data);
    const orderID = data.orderID;

    return axios
        .post("/paypal/rest-api/capture-order", {
            orderID: orderID,
        })
        .then((res) => {
            console.log("success creating order", res.data);
        })
        .catch((err) => {
            console.error(err);
        });
};

Solution

  • The exception should be an instance of BadResponseException which has a getResponse method. You can then cast the response body to a string.

    $response = json_decode($ex->getResponse()->getBody()->getContents(), true);