javaspringspring-bootopenai-apihttp-status-code-403

Endpoint returning 403 after successfully executing (Spring Boot)


I made an endpoint "/api/chat" that, when called, makes another request to the OpenAI API (paid for and API key valid, all that). When testing the endpoint in Postman, I first request my /auth/login endpoint to get a valid JWT login token, copy that for the Authorization header of my /api/chat call, then send it (The login auth token is 100% valid, and works perfectly for all other endpoints as well).

When the request to /api/chat is made, Postman takes around 1.5-2 seconds to process it. In the first second, my Run Console in Intellij logs the correct things to signify a request being made. For the remaining time, it then logs the exact expected response from the OpenAI API model. You would think this means everything executed fine, and it seemingly did, but then Postman (and my website if you try it there), returns 403 instead of 200 OK. This is an issue because even though I can physically see the correct response being logged in my IDE console with no errors, the 403 means I can't extract any of it for use and the website can't receive it and treats it like a completely failed request.

The ONLY way that I "fixed" this was making the /api/chat endpoint public in my SecurityConfig file, which made it execute the exact same way and give the same result, but returns 200 OK as expected. However, I, of course, can't leave it like this as it would mean people have public access to make calls to an endpoint that costs me for each request.

I've spent many hours trying to figure this out, including Googling, asking GPT - nothing. Verified all my JWT authentication stuff, tried something where the auth token is retained throughout, nothing worked. CORS and CSRF stuff, seemingly not that either. I can only think its something up with Spring Security because adding the endpoint to the .permitAll() list (making it public) in SecurityConfig is the only thing that made it return 200 OK as expected. This is extremely frustrating because it executes and logs exactly how and what it should, but just for some reason returns 403.

This is my first project with Spring so if its something trivial then please help out. Thanks in advance. The API key is set in my env variables, and even though I've included some error logging, no errors are ever logged, only the correct responses that I'm expecting from the AI model, with it then returning 403 as I've mentioned. I'll include the relevant bits of code, but if you think the issue is in another file or part of these files, let me know and I'll share what I can. Not that it's too important I don't think, but I'm using React for frontend.

:

Security Config:

@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf()
                .disable()
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/auth/**", "/", "/index.html", "/manifest.json", "/static/**", "/*.js", "/*.jsx", "/*.css", "/home", "/log-in", "/sign-up")
                        .permitAll()
                        .requestMatchers("/auth/signup", "/auth/login").anonymous()
                        .anyRequest()
                        .authenticated()
                )
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .authenticationProvider(authenticationProvider)
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();

Controller:

@PostMapping
    public Mono<ResponseEntity<String>> getChatCompletion(@RequestBody ChatRequest chatRequest, @RequestHeader HttpHeaders headers) {

        return openAiService.getChatCompletion(chatRequest.getInput())
                .map(response -> {
                    logger.info("Response: {}", response); // Logging response
                    return ResponseEntity.ok(response);
                })
                .defaultIfEmpty(ResponseEntity.noContent().build());
    }

    public static class ChatRequest {
        private String input;

        public String getInput() {
            return input;
        }

        public void setInput(String input) {
            this.input = input;
        }
    }

Service:

public Mono<String> getChatCompletion(String userInput) {
        String requestBody = String.format("""
                {
                    "model": "gpt-3.5-turbo",
                    "messages": [
                        {
                            "role": "system",
                            "content": "MY CONTENT"
                        },
                        {
                            "role": "user",
                            "content": "%s"
                        }
                    ],
                    "temperature": 1,
                    "max_tokens": 256,
                    "top_p": 1,
                    "frequency_penalty": 0,
                    "presence_penalty": 0
                }
                """, userInput);

        logger.info("Sending request to OpenAI with body: {}", requestBody);

        return this.webClient.post()
                .uri("/chat/completions")
                .header("Content-Type", "application/json")
                .header("Authorization", "Bearer " + openaiApiKey)
                .bodyValue(requestBody)
                .retrieve()
                .bodyToMono(String.class)
                .doOnNext(response -> logger.info("Received response from OpenAI: {}", response))
                .doOnError(WebClientResponseException.class, error -> {
                    logger.error("Error response from OpenAI: {}", error.getResponseBodyAsString());
                })
                .doOnError(error -> logger.error("Error occurred: ", error));
    }

My Postman request (not too relevant, this is not the issue):

{
input: "MY INPUT"
}

Solution

  • I tried adding the @ResponseBody but that didn't solve it. After more digging, it seems it was an issue with the Mono<ResponseEntity> return type, as that is telling Spring to process the request asynchronously and non-blocking. Converting the method to block with ".block()" on the Mono, and making it just a normal blocking call allowed it to wait for a response from the service before proceeding. Kinda means you don't get the benefits of a reactive programming solution but at least we can progress with this now.

    If you run into the same issue, try converting the call to a traditional blocking call and see if that works. Still don't really know what in Spring's reactive code is causing this but I'm glad to just move on for now.