Next.js proxy 500 ERROR

김도비·2025년 1월 10일
post-thumbnail

FE(Next.js)에서 await ApiService.call()로 BE(Spring Boot) 호출 중 POST http://localhost:3000/api/proxy 500 (Internal Server Error) 에러와 마주쳤다.

호출 구조는
FE → localhost:3000/api/proxy → localhost:9300/api/transaction → BE TransactionController
순서였다.

로컬 테스트 중이고 서버 측 에러라고 생각해
SecurityConfig, WebMvcConfig(WebMvcConfigurer) 등
Spring Boot의 CORS 설정을 수정했는데
여전히 /api/proxy 500 Error가 발생했다.

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/apis/**")
                .allowedOrigins("http://localhost:3000") // Next.js 서버의 주소
                .allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.OPTIONS.name())
                .allowCredentials(true)
                .maxAge(3600);
    }
}
@Configuration
@EnableWebSecurity
public class HttpSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and() // CORS 허용
            .authorizeRequests()
            .antMatchers("/api/**").permitAll() 
            .anyRequest().authenticated();
    }
}
@Bean
public FilterRegistrationBean<CORSFilter> corsFilter() {
    FilterRegistrationBean<CORSFilter> registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new CORSFilter());
    registrationBean.addUrlPatterns("/apis/**"); // 경로에 대한 CORS 필터 적용
    return registrationBean;
}

다 설정 했는데...

No Response면 Spring Boot 서버가 요청을 차단했거나 응답을 반환하지 못하는 경우 아닌가...?


구글링에 디버깅에 콘솔 로그까지 다 찍어봤지만 원인을 찾을 수 없어 결국 ChatGPT에 소스 코드를 모두 복붙해 원인을 찾아내게 시켰다.

ChatGPT 찬스를 사용했음에도 불구하고

  1. CORS 설정 중복을 피하고, 정확한 도메인만 설정합니다.
  2. CORSFilter와 WebMvcConfigurer의 중복을 제거합니다.
  3. fetch 요청의 헤더와 서버의 처리 방식이 일치하는지 확인합니다.
  4. 서버 로그에서 500 에러의 원인을 확인하고, 예외가 발생한 부분을 분석합니다.
  5. Postman으로 서버 API를 테스트하여 클라이언트와 서버 간의 문제를 구분합니다.

라고만 알려줬고, 여전히 Response는 Null이었다 ㅜㅜ

const response = await fetch('http://localhost:3000/api/proxy', request);

여기서 왜 계속 막히는 거냐구!!!


돌고 돌아 프록시 서버 로그를 확인하는 데에 집중했다.

export async function POST(req: NextRequest, res: NextResponse) {
  const session = await auth();
  const error = session?.error;

  let formData = null;
  let json = null;

  try {
    formData = await req.formData();
  } catch (e) {
    console.error('Not FormData');
  }

  try {
    json = await req.json();
  } catch (e) {
    console.error('Not JSON');
  }

  if (session && !error) {
    let response = null;
    const requestInit = {
      cache: 'no-cache' as RequestCache,
      headers: {
        'Authorization': Bearer ${session.access_token}
      }
    };

    if (json) {
      const { command: apiCommand, body } = json;
      const apiItem = _.get(apiURLs, _.split(apiCommand, '.'));
      const { method = 'GET', contentType = 'application/json', url } = apiItem;

      let apiUrl = translateUrlJson(url, body);

      _.set(requestInit, 'method', method);
      _.set(requestInit.headers, 'Content-Type', contentType);

      switch (method) {
        case 'GET':
          if (_.keys(body).length > 0) {
            const querystring = new URLSearchParams(body).toString();
            apiUrl += ?${querystring};
          }
          break;

        case 'POST':
          _.set(requestInit, 'body', JSON.stringify(body));
          break;
      }

      response = await fetch(apiUrl, requestInit);
    }

    if (formData) {
      const apiCommand = formData.get(API_COMMAND_KEY) as string;
      formData.delete(API_COMMAND_KEY);

      const apiItem = _.get(apiURLs, _.split(apiCommand, '.'));
      const { method = 'POST', contentType = 'multipart/form-data', url } = apiItem;

      const { url: apiUrl, data: updatedFormData } = translateUrlFormData(url, formData);

      _.set(requestInit, 'method', method);
      _.set(requestInit, 'body', updatedFormData);

      response = await fetch(apiUrl, requestInit);
    }

    if (response != null) {
      const responseContentType = response.headers.get('Content-Type');

      if (responseContentType === 'application/zip') {
        return new NextResponse(await response.blob(), {
          status: response.status,
          statusText: response.statusText,
          headers: {
            'Content-Type': 'application/zip',
            'Content-Disposition': 'attachment; filename="download.zip"'
          }
        });
      }

      return NextResponse.json(await response.json(), { status: response.status, statusText: response.statusText });
    } else {
      return NextResponse.json({ message: 'No Response' }, { status: 500 });
    }
  } else {
    return NextResponse.json({ message: 'You must be logged in.' }, { status: 401 });
  }
}

Spring과 통신하는 route.ts 코드인데
json이나 formData로 파싱하는 과정에서 오류가 발생할 수 있다는 글을 보았다.

그래서 아래와 같이 로그를 찍어 확인해보았는데

try {
    const rawBody = await req.text();
    console.log('[Raw Body]', rawBody);

    json = await req.json();
  } catch (e) {
    console.error('Error parsing JSON:', e);
  }

Error parsing JSON: TypeError: Body is unusable: Body has already been read 라는 에러가 떴다.

이 에러는 HTTP 요청의 body를 한 번 읽은 후에는 다시 읽을 수 없다는 스트림 처리의 특성에서 발생하는 문제라고 한다.

let json = await req.json();
let formData = await req.formData();

현재 위와 같이 파싱하고 있었는데

아래의 파싱으로 로직을 수정했다.

const body = await req.text();
let json = JSON.parse(body);
let formData = new URLSearchParams(body);

호출해보니 Spring Boot Controller 진입도 정상이고 데이터도 잘 가져오고 200 OK 떨어졌다!!!

profile
Java Backend 4년차 Developer

0개의 댓글