Wednesday, December 11, 2024

Calling 3rd Party APIs using RestTemplate - Have you handled all exceptions? Are you sure?

Let’s demonstrate the use of RestTemplate to call a third-party API with JWT Bearer authentication. This implementation includes detailed error handling for various HTTP status codes, connection issues, serialization errors, and more. All errors are logged appropriately, and meaningful error responses are returned to the API caller.

Overview

  1. Adding JWT Bearer Token: The implementation includes adding a JWT Bearer token to the request headers.
  2. Comprehensive Error Handling: Specific handling for different 4xx and 5xx HTTP status codes, connection issues, and serialization errors.
  3. Logging: All errors and important steps are logged using SLF4J.
  4. Error Response: Custom error responses are sent back to the API caller indicating why the external API call failed.

Dependencies

Ensure you have the following dependencies in your pom.xml:

<dependencies>
    <!-- Spring Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- SLF4J for Logging -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>

    <!-- Jackson for JSON Processing -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>

    <!-- Optional: For Circuit Breaker (Resilience4j) -->
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-spring-boot2</artifactId>
        <version>1.7.1</version>
    </dependency>
</dependencies>

Implementation

1. Custom Error Response Class

Create a class to represent the error response structure sent back to the API caller.

// src/main/java/com/example/demo/response/ErrorResponse.java
package com.example.demo.response;

public class ErrorResponse {
    private String error;
    private String message;
    private int status;

    public ErrorResponse() {}

    public ErrorResponse(String error, String message, int status) {
        this.error = error;
        this.message = message;
        this.status = status;
    }

    // Getters and Setters

    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }
}

2. Service Class with RestTemplate Call

Implement the service class that makes the external API call with comprehensive error handling.

// src/main/java/com/example/demo/service/ExternalApiService.java
package com.example.demo.service;

import com.example.demo.response.ErrorResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.*;

@Service
public class ExternalApiService {

    private static final Logger logger = LoggerFactory.getLogger(ExternalApiService.class);
    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper;

    public ExternalApiService(RestTemplate restTemplate, ObjectMapper objectMapper) {
        this.restTemplate = restTemplate;
        this.objectMapper = objectMapper;
    }

    public ResponseEntity<?> callThirdPartyApi(String endpoint, String jwtToken, Object requestPayload) {
        String apiUrl = "<https://api.thirdparty.com/>" + endpoint; // Replace with actual API URL

        // Set up headers with JWT Bearer token
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.setBearerAuth(jwtToken);

        HttpEntity<Object> entity = new HttpEntity<>(requestPayload, headers);

        try {
            logger.info("Initiating {} request to {}",
                        requestPayload == null ? "GET" : "POST", apiUrl);

            ResponseEntity<String> response;

            if (requestPayload == null) {
                // GET request
                response = restTemplate.exchange(apiUrl, HttpMethod.GET, entity, String.class);
            } else {
                // POST request
                response = restTemplate.exchange(apiUrl, HttpMethod.POST, entity, String.class);
            }

            logger.info("Received successful response from external API. Status Code: {}", response.getStatusCode());

            // Optionally, deserialize response to a specific class
            // Example:
            // MyResponse myResponse = objectMapper.readValue(response.getBody(), MyResponse.class);
            // return ResponseEntity.ok(myResponse);

            return ResponseEntity.ok(response.getBody());

        } catch (HttpClientErrorException e) {
            // Handle 4xx errors
            logger.error("Client error while calling external API. Status Code: {}, Response Body: {}",
                         e.getStatusCode(), e.getResponseBodyAsString(), e);
            return createErrorResponse(e.getStatusCode(), e.getResponseBodyAsString());

        } catch (HttpServerErrorException e) {
            // Handle 5xx errors
            logger.error("Server error while calling external API. Status Code: {}, Response Body: {}",
                         e.getStatusCode(), e.getResponseBodyAsString(), e);
            return createErrorResponse(e.getStatusCode(), "External service is unavailable. Please try again later.");

        } catch (ResourceAccessException e) {
            // Handle connection issues (e.g., timeout)
            logger.error("Connection error while calling external API. Message: {}", e.getMessage(), e);
            return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT)
                                 .body(new ErrorResponse("Connection Error", "Unable to reach external service.", HttpStatus.GATEWAY_TIMEOUT.value()));

        } catch (RestClientException e) {
            // Handle other RestTemplate errors
            logger.error("RestClientException occurred while calling external API. Message: {}", e.getMessage(), e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                                 .body(new ErrorResponse("Internal Error", "An error occurred while processing the request.", HttpStatus.INTERNAL_SERVER_ERROR.value()));

        } catch (JsonProcessingException e) {
            // Handle JSON parsing errors
            logger.error("JSON processing error while handling external API response. Message: {}", e.getMessage(), e);
            return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
                                 .body(new ErrorResponse("Parsing Error", "Failed to parse external service response.", HttpStatus.BAD_GATEWAY.value()));

        } catch (Exception e) {
            // Catch any other unexpected errors
            logger.error("Unexpected error while calling external API. Message: {}", e.getMessage(), e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                                 .body(new ErrorResponse("Unexpected Error", "An unexpected error occurred.", HttpStatus.INTERNAL_SERVER_ERROR.value()));
        }
    }

    private ResponseEntity<ErrorResponse> createErrorResponse(HttpStatus status, String message) {
        ErrorResponse errorResponse = new ErrorResponse(status.getReasonPhrase(), message, status.value());
        return new ResponseEntity<>(errorResponse, status);
    }
}

3. Controller to Expose the API Endpoint

Create a controller that uses the ExternalApiService to handle incoming API requests and respond accordingly.

// src/main/java/com/example/demo/controller/ApiController.java
package com.example.demo.controller;

import com.example.demo.service.ExternalApiService;
import com.example.demo.response.ErrorResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class ApiController {

    @Autowired
    private ExternalApiService externalApiService;

    /**
     * Example endpoint to get data from a third-party service.
     * Replace 'get-data' and requestPayload as needed.
     */
    @GetMapping("/get-data")
    public ResponseEntity<?> getData(@RequestParam String param, @RequestHeader("Authorization") String authHeader) {
        String jwtToken = extractJwtToken(authHeader);
        return externalApiService.callThirdPartyApi("get-data?param=" + param, jwtToken, null);
    }

    /**
     * Example endpoint to post data to a third-party service.
     * Replace 'post-data' and requestPayload as needed.
     */
    @PostMapping("/post-data")
    public ResponseEntity<?> postData(@RequestBody Object requestPayload, @RequestHeader("Authorization") String authHeader) {
        String jwtToken = extractJwtToken(authHeader);
        return externalApiService.callThirdPartyApi("post-data", jwtToken, requestPayload);
    }

    private String extractJwtToken(String authHeader) {
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }
        throw new IllegalArgumentException("Invalid Authorization header.");
    }
}

4. Global Exception Handler (Optional but Recommended)

To handle any exceptions not caught within the service layer, you can use @ControllerAdvice for centralized exception handling.

// src/main/java/com/example/demo/exception/GlobalExceptionHandler.java
package com.example.demo.exception;

import com.example.demo.response.ErrorResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;

@ControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgs(IllegalArgumentException ex) {
        logger.error("IllegalArgumentException: {}", ex.getMessage(), ex);
        ErrorResponse error = new ErrorResponse("Bad Request", ex.getMessage(), HttpStatus.BAD_REQUEST.value());
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

    // Add more exception handlers as needed

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) {
        logger.error("Unhandled exception: {}", ex.getMessage(), ex);
        ErrorResponse error = new ErrorResponse("Internal Server Error", "An unexpected error occurred.", HttpStatus.INTERNAL_SERVER_ERROR.value());
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

5. Configuration for RestTemplate (Optional: Timeout Settings)

Configure RestTemplate with timeout settings to handle scenarios where the external API is slow to respond.

// src/main/java/com/example/demo/config/RestTemplateConfig.java
package com.example.demo.config;

import org.springframework.context.annotation.*;
import org.springframework.http.client.*;
import org.springframework.web.client.RestTemplate;

import java.time.Duration;

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        // Customize the RestTemplate if needed
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(5000); // 5 seconds
        factory.setReadTimeout(5000); // 5 seconds

        return new RestTemplate(factory);
    }
}

6. Sample Request and Response

a. GET Request Example

  • Endpoint: /api/get-data?param=value
  • Headers:
    • Authorization: Bearer your_jwt_token

b. POST Request Example

  • Endpoint: /api/post-data

  • Headers:

    • Authorization: Bearer your_jwt_token
    • Content-Type: application/json
  • Body:

    {
        "key1": "value1",
        "key2": "value2"
    }
    
    

c. Successful Response

{
    "data": "Successful response from external API"
}

d. Error Response Example (Client Error)

{
    "error": "Bad Request",
    "message": "Invalid parameters provided.",
    "status": 400
}

Detailed Error Handling Implementation

Let's dive deeper into how specific HTTP status codes are handled within the ExternalApiService.

1. Handling 4xx Client Errors

Each 4xx status code indicates a client-side error. Here's how you can handle some common 4xx errors:

  • 400 Bad Request: The request was malformed or invalid.
  • 401 Unauthorized: Authentication failed or missing.
  • 403 Forbidden: The client does not have access rights.
  • 404 Not Found: The requested resource does not exist.
  • 429 Too Many Requests: Rate limiting has been applied.

Implementation:

catch (HttpClientErrorException e) {
    HttpStatus status = e.getStatusCode();
    String responseBody = e.getResponseBodyAsString();

    switch (status) {
        case BAD_REQUEST:
            logger.error("400 Bad Request: {}", responseBody, e);
            return createErrorResponse(status, "Invalid request parameters.");

        case UNAUTHORIZED:
            logger.error("401 Unauthorized: {}", responseBody, e);
            return createErrorResponse(status, "Authentication failed. Please check your credentials.");

        case FORBIDDEN:
            logger.error("403 Forbidden: {}", responseBody, e);
            return createErrorResponse(status, "You do not have permission to access this resource.");

        case NOT_FOUND:
            logger.error("404 Not Found: {}", responseBody, e);
            return createErrorResponse(status, "The requested resource was not found.");

        case TOO_MANY_REQUESTS:
            logger.error("429 Too Many Requests: {}", responseBody, e);
            return createErrorResponse(status, "Rate limit exceeded. Please try again later.");

        default:
            logger.error("{} Error: {}", status.value(), responseBody, e);
            return createErrorResponse(status, "Client error occurred.");
    }
}

2. Handling 5xx Server Errors

Server-side errors indicate issues with the external API.

  • 500 Internal Server Error: Generic server error.
  • 502 Bad Gateway: Invalid response from the upstream server.
  • 503 Service Unavailable: The server is not ready to handle the request.
  • 504 Gateway Timeout: The server did not receive a timely response.

Implementation:

catch (HttpServerErrorException e) {
    HttpStatus status = e.getStatusCode();
    String responseBody = e.getResponseBodyAsString();

    switch (status) {
        case INTERNAL_SERVER_ERROR:
            logger.error("500 Internal Server Error: {}", responseBody, e);
            return createErrorResponse(status, "External service encountered an error. Please try again later.");

        case BAD_GATEWAY:
            logger.error("502 Bad Gateway: {}", responseBody, e);
            return createErrorResponse(status, "Received invalid response from external service.");

        case SERVICE_UNAVAILABLE:
            logger.error("503 Service Unavailable: {}", responseBody, e);
            return createErrorResponse(status, "External service is currently unavailable. Please try again later.");

        case GATEWAY_TIMEOUT:
            logger.error("504 Gateway Timeout: {}", responseBody, e);
            return createErrorResponse(status, "External service timed out. Please try again later.");

        default:
            logger.error("{} Server Error: {}", status.value(), responseBody, e);
            return createErrorResponse(status, "Server error occurred.");
    }
}

3. Handling Connection Issues and Timeouts

ResourceAccessException is thrown when there are connection issues, such as timeouts or DNS failures.

Implementation:

catch (ResourceAccessException e) {
    logger.error("Connection error while calling external API. Message: {}", e.getMessage(), e);
    return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT)
                         .body(new ErrorResponse("Connection Error", "Unable to reach external service.", HttpStatus.GATEWAY_TIMEOUT.value()));
}

4. Handling JSON Serialization/Deserialization Errors

If you expect a specific response structure, deserialize the JSON response into a Java object. Handle any JsonProcessingException that may occur during this process.

Implementation:

try {
    // Assuming you have a ResponseClass to map the response
    ResponseClass responseObj = objectMapper.readValue(response.getBody(), ResponseClass.class);
    return ResponseEntity.ok(responseObj);
} catch (JsonProcessingException e) {
    logger.error("JSON parsing error while handling external API response. Message: {}", e.getMessage(), e);
    return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
                         .body(new ErrorResponse("Parsing Error", "Failed to parse external service response.", HttpStatus.BAD_GATEWAY.value()));
}

Best Practices Implemented

  1. JWT Bearer Token Management: Tokens are extracted from the Authorization header and added to the external API request headers securely.
  2. Specific Error Handling: Different 4xx and 5xx errors are handled explicitly, providing clear and actionable error messages.
  3. Logging: All errors and significant steps are logged with appropriate severity levels (info, error).
  4. Fallback Responses: In case of errors, meaningful fallback responses are returned to the API caller without exposing internal details.
  5. Configuration Management: Timeout settings are configured to prevent the application from hanging indefinitely.
  6. Centralized Exception Handling: A @ControllerAdvice is used to handle exceptions that may occur outside the service layer, ensuring consistency.

Testing the Implementation

To test the implementation, you can use tools like Postman or cURL to make requests to your API endpoints.

Example cURL Commands:

  1. GET Request
curl -X GET "<http://localhost:8080/api/get-data?param=value>" \\\\
     -H "Authorization: Bearer your_jwt_token"

  1. POST Request
curl -X POST "<http://localhost:8080/api/post-data>" \\\\
     -H "Authorization: Bearer your_jwt_token" \\\\
     -H "Content-Type: application/json" \\\\
     -d '{"key1": "value1", "key2": "value2"}'

Conclusion

This implementation ensures that all possible errors during external API calls are handled gracefully. It provides detailed logging for troubleshooting and returns clear, structured error responses to the API consumers. Additionally, by incorporating JWT Bearer authentication, it secures the external API requests. You can further enhance this setup by integrating circuit breakers, retries, and more advanced resilience patterns as needed.

No comments:

Post a Comment