1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ResponseStatusException のメッセージが返らない理由と、正しく返すための設定方法

1
Posted at

Spring Boot × React でハマった落とし穴と対策まとめ

簡単に

現象
ResponseStatusException(HttpStatus.UNAUTHORIZED, "メッセージ")のメッセージがフロントに届かない

原因(2 段階)

  1. サーバー側
    Spring Bootのデフォルト設定では ResponseStatusException のreasonがJSONに含まれない
  2. フロント側
    ProblemDetailの Content-Type: application/problem+jsonapplication/json 決め打ちの判定が拾えず、JSONが文字列として扱われる

対策(両方必要)

  • バックエンド: spring.mvc.problemdetails.enabled=true を有効化してProblemDetailを返す
  • フロント: Content-Type 判定を includes('json') に緩める

環境

  • バックエンド: Spring Boot + Spring Security
  • フロント: React + Vite
  • fetchベースの自作 HTTP ラッパ
  • 認証方式: セッション (JSESSIONID)

起きた問題

ログイン API はバックエンドで明示的にメッセージを指定しているのに、

throw new ResponseStatusException(
    HttpStatus.UNAUTHORIZED,
    "ユーザー名またはパスワードが正しくありません"
);

フロントでは 汎用エラー しか表示されない。

const serverMsg = extractServerMessage(payload)
throw new HttpError(serverMsg ?? `POST ${path} failed`, res.status, 

Networkタブを確認すると、

{
  "timestamp": "...",
  "status": 401,
  "error": "Unauthorized",
  "path": "/api/auth/login"
}

message が無い

原因1: Spring Bootがreasonを返していない

Spring Boot 2.3 以降、セキュリティ強化のため
server.error.include-message=never
がデフォルト。
そのため、ResponseStatusException の reason はJSONに含まれない

対策: ProblemDetail を有効化

application.properties に追加:

spring.mvc.problemdetails.enabled=true

すると ResponseStatusException は RFC 7807 形式の ProblemDetail で返るようになる。

{
  "detail": "ユーザー名またはパスワードが正しくありません",
  "instance": "/api/auth/login",
  "status": 401,
  "title": "Unauthorized"
}

detail にメッセージが入るので、フロントで拾えるはず...
と思いきや、まだ動かない。

原因2: Content-Type 判定が厳しすぎた

フロントの readPayload を確認。

async function readPayload(res: Response): Promise<unknown> {
  const ct = res.headers.get('content-type') ?? ''
  if (ct.includes('application/json')) return await res.json()
  const text = await res.text()
  return text.length ? text : null
}

しかし ProblemDetail の Content-Type は…
Content-Type: application/problem+json

Content-Type includes('application/json')
application/json true
application/problem+json false

"application/problem+json" の中に "application/json" という連続部分は存在しないため、JSONとしてパースされず、ただの文字列として扱われる

結果:

payload = '{"detail":"...",...}'

extractServerMessage はオブジェクトのみ対象なので null を返す。
結果として serverMsg ?? fallbackfallback が選ばれて、汎用エラーが表示されていた。

function extractServerMessage(payload: unknown): string | null {
  if (payload && typeof payload === 'object') {
    const obj = payload as Record<string, unknown>
    if (typeof obj.detail === 'string') return obj.detail   // ProblemDetail
    if (typeof obj.message === 'string') return obj.message // 旧 Boot エラー形式
  }
  return null
}

対策: Content-Type 判定を緩める

- if (ct.includes('application/json')) return await res.json()
+ if (ct.includes('json')) return await res.json()

+json 系( application/problem+json など)も拾えるようになる。

最終的な対策まとめ

バックエンド (application.properties)

spring.mvc.problemdetails.enabled=true

フロント (fetch ラッパ)

async function readPayload(res: Response): Promise<unknown> {
  const ct = res.headers.get('content-type') ?? ''
  if (ct.includes('json')) return await res.json()   // ★ ここ
  const text = await res.text()
  return text.length ? text : null
}

これでフロントに 「ユーザー名またはパスワードが正しくありません」 が正しく表示される。

なぜ気づきにくいのか

  • Devtools のResponse プレビューは Content-Type に関係なく JSON を整形表示する
    →「ちゃんと JSON が返ってる」と錯覚しやすい
  • res.json() はContent-Typeを見ない
    →自前で Content-Type 判定を入れると、落とし穴になりやすい
  • extractServerMessage のガードが堅牢で
    →「文字列で来ている」という異常が表に出ない(静かに null になる)

学び・教訓

1. Spring Boot の ResponseStatusException はデフォルトで reason が返らない

  • server.error.include-message=always で雑に出す
  • spring.mvc.problemdetails.enabled=true で ProblemDetail として明示的に返す
  • もしくは@RestControllerAdvice で個別にハンドリングする

2. ProblemDetail は application/problem+json で返る

  • フロントの Content-Type 判定を application/json 決め打ちにしない
  • includes('json')+json 系を許容する正規表現などで緩める

3. fetch ラッパの設計ポイント

  • 成功時だけでなく エラー時も JSON をパースできる ようにしておく
  • 4xx/5xx で Content-Type が変わる API は珍しくない
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?