# Http Call

## Open Feign

### Introduction

* The Feign client is a **declarative** **REST client** that makes writing web clients easier. When using Feign, the developer has **only to define the interfaces** and annotate them accordingly. The actual web client implementation is then provided by Spring at runtime.
* For each request, the **assigned thread blocks until it receives the response**. The disadvantage of **keeping multiple threads alive** is that **each open thread occupies memory and CPU cycles**.

### Implementation

* Add the dependency, Example: Maven

{% code title="pom.xml" %}

```xml
<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
```

{% endcode %}

* Setting up environment variable

{% code title="application.yaml" %}

```yaml
chat:
  api:
    url: http://localhost:8000    
    key: your-super-secret-api-key
```

{% endcode %}

* Setting up Http header configuration and error handling

{% code title="ChatFeignConfig.java" %}

```java
package com.example.maven_playground.config;
import feign.RequestInterceptor;
import feign.codec.ErrorDecoder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatFeignConfig {

    @Value("${chat.api.key}")
    private String apiKey;

    @Bean
    public RequestInterceptor chatApiRequestInterceptor() {
        return requestTemplate -> {
            requestTemplate.header("Content-Type", "application/json");
            requestTemplate.header("X-API-Key", apiKey);
        };
    }

    @Bean
    public ErrorDecoder chatErrorDecoder() {
        return new ChatFeignErrorDecoder();
    }
}
```

{% endcode %}

{% code title="ChatFeignErrorDecoder.java" %}

```java
package com.example.maven_playground.config;

import com.example.maven_playground.dto.ChatErrorResponse;
import com.example.maven_playground.exception.ChatApiException;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;

import java.io.IOException;
import java.io.InputStream;
// Decode the response from another service
@Slf4j
public class ChatFeignErrorDecoder implements ErrorDecoder {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public Exception decode(String methodKey, Response response) {
        HttpStatus status = HttpStatus.valueOf(response.status());
        String detail = "Unknown error from Chat API";

        try (InputStream bodyIs = response.body().asInputStream()) {
            ChatErrorResponse errorResponse = objectMapper.readValue(
                    bodyIs,
                    ChatErrorResponse.class);
            if (errorResponse.getDetail() != null) {
                detail = errorResponse.getDetail();
            }
        } catch (IOException e) {
            log.error("Error reading error response body", e);
        }

        log.error("Chat API error - Status: {}, Detail: {}", status, detail);
        return new ChatApiException(status, detail);
    }
}
```

{% endcode %}

```java
package com.example.maven_playground.exception;

import com.example.maven_playground.dto.ChatErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

// Handle the output response of http call
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(ChatApiException.class)
    public ResponseEntity<ChatErrorResponse> handleChatApiException(ChatApiException ex) {
        log.error("Chat API error: {}", ex.getDetail());

        ChatErrorResponse errorResponse = ChatErrorResponse.builder()
                .detail(ex.getDetail())
                .build();

        return ResponseEntity.status(ex.getStatus()).body(errorResponse);
    }
}
```

* Define the class of output and error class of http call response

{% code title="ChatResponse.java" %}

```java
package com.example.maven_playground.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatResponse {
    // Add fields based on the actual API response
    // For now, assuming a simple response structure
    private String answer;
    private String status;

    // Add more fields as needed based on actual API response
}
```

{% endcode %}

{% code title="ChatErrorResponse.java" %}

```java
package com.example.maven_playground.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatErrorResponse {
    private String detail;
}
```

{% endcode %}

* Define the client(interface) of HTTP call

{% code title="ChatClient.java" %}

```java
package com.example.maven_playground.client;

import com.example.maven_playground.config.ChatFeignConfig;
import com.example.maven_playground.dto.ChatRequest;
import com.example.maven_playground.dto.ChatResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(name = "chatClient", url = "${chat.api.url}", 
configuration = ChatFeignConfig.class)
public interface ChatClient {

    @PostMapping("/chat")
    ChatResponse chat(
            @RequestBody ChatRequest request);
}

```

{% endcode %}

```java
@Service
@Slf4j
public class ChatService {
    @Autowired
    private ChatClient chatClient;

    public ChatResponse askQuestion(String question) {
        try {
            log.info("Sending question to chat API: {}", question);

            ChatRequest request = ChatRequest.builder()
                    .question(question)
                    .build();

            ChatResponse response = chatClient.chat(request);

            log.info("Received response from chat API: {}", response);
            return response;
        } catch (Exception e) {
            log.error("Error sending question to chat API: {}", e.getMessage());
            throw new RuntimeException("Error sending question to chat API", e);
        }
    }
}
```

## Web Client

### Introduction

* The *WebClient* is part of the [Spring WebFlux](https://www.baeldung.com/spring-webflux) library. It’s **a non-blocking solution provided by the Spring Reactive Framework**
* *WebClient* executes the HTTP request and **adds a “waiting for response” task into a queue**. Later, **the “waiting for response” task is executed from the queue after the response is received**, finally delivering the response to the subscriber function.

### Implementation

* Add the dependencies

{% code title="pom.xml" %}

```xml
<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
```

{% endcode %}

* Setting up web client with http call detail

{% code title="ChatStreamClient.java" %}

```java
package com.example.maven_playground.client;

import com.example.maven_playground.dto.ChatRequest;
import com.example.maven_playground.exception.ChatApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;

@Component
@Slf4j
public class ChatStreamClient {

    private final WebClient webClient;

    public ChatStreamClient(
            @Value("${chat.api.url}") String baseUrl,
            @Value("${chat.api.key}") String apiKey) {

        this.webClient = WebClient.builder()
                .baseUrl(baseUrl)
                .defaultHeader("X-API-Key", apiKey)
                .defaultHeader("Content-Type", "application/json")
                .build();
    }

    public Flux<String> chatStream(ChatRequest request) {
        log.info("Starting SSE stream for question: {}", request.getQuestion());

        return webClient.post()
                .uri("/chat/stream")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.TEXT_EVENT_STREAM)
                .bodyValue(request)
                .retrieve()
                .onStatus(
                        status -> status.isError(),
                        response -> response.bodyToMono(String.class)
                                .map(body -> {
                                    log.error("Stream API error: {}", body);
                                    return new ChatApiException(
                                            HttpStatus.valueOf(response.statusCode().value()),
                                            extractDetail(body));
                                }))
                .bodyToFlux(String.class)
                .doOnNext(data -> log.debug("Received SSE data: {}", data))
                .doOnError(e -> log.error("Stream error: {}", e.getMessage()))
                .doOnComplete(() -> log.info("Stream completed"));
    }

    private String extractDetail(String body) {
        // Try to extract "detail" from JSON error response
        if (body != null && body.contains("\"detail\"")) {
            try {
                int start = body.indexOf("\"detail\"") + 10;
                int end = body.indexOf("\"", start);
                return body.substring(start, end);
            } catch (Exception e) {
                return body;
            }
        }
        return body != null ? body : "Unknown error";
    }
}
```

{% endcode %}

* Define the class of streaming response and controller level for response type

{% code title="ChatStreamResponse.java" %}

```java
package com.example.maven_playground.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ChatStreamResponse {
    private String content;
    private String event;
    private boolean done;
}
```

{% endcode %}

{% code title="ChatService.java" %}

```java
package com.example.maven_playground.service;

import com.example.maven_playground.client.ChatClient;
import com.example.maven_playground.client.ChatStreamClient;
import com.example.maven_playground.dto.ChatRequest;
import com.example.maven_playground.dto.ChatResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

@Service
@Slf4j
public class ChatService {

    @Autowired
    private ChatStreamClient chatStreamClient;

    public Flux<String> askQuestionStream(String question) {
        log.info("Starting streaming question to chat API: {}", question);

        ChatRequest request = ChatRequest.builder()
                .question(question)
                .build();

        return chatStreamClient.chatStream(request);
    }
}

```

{% endcode %}

{% code title="ChatController.java" %}

```java
package com.example.maven_playground.controllers;

import com.example.maven_playground.dto.ChatRequest;
import com.example.maven_playground.dto.ChatResponse;
import com.example.maven_playground.service.ChatService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    @Autowired
    private ChatService chatService;

    @PostMapping(value = "/stream", produces = "text/event-stream;charset=UTF-8")
    public Flux<String> chatStream(@RequestBody ChatRequest request) {
        return chatService.askQuestionStream(request.getQuestion());
    }
}

```

{% endcode %}

## Comparison

* **Open Feign** will be more developer-friendly, easy to implement through interface layer. Most are likely suitable for **CRUD** operation
* For handling streaming, Feign’s design is **request → response → close connection, streaming is not supported out of the box.**  In that case, Web Client will be better.
* Although the Feign client is a great choice in many cases and the resulting code has lower cognitive complexity, the non-blocking style of *WebClient* uses much **fewer system resources** during **high-traffic spikes**
