著者: 著者: やまねこ タグ:
旧エンドポイント(app.rakuten.co.jp)から新エンドポイント(openapi.rakuten.co.jp)への移行を示すダークテーマのイラスト。2つのサーバーノードが光るアローで接続されている

2026年5月14日に楽天 Web Service の旧ドメイン(app.rakuten.co.jp)が停止する予定です。このブログでは商品カードのデータを静的ビルド時に楽天 API から取得しているため、対応しなければ daily-product-update.yml の cron ジョブが止まります。移行前後に詰まった 403 エラーの記録を残します。

移行に必要な変更は2点でした。エンドポイントの差し替えと、accessKey の追加です。ただし Node 22.x 環境で fetch() を使っていた場合は undici.request() への切り替えも判断することになりました。GitHub Actions で動かしている場合は Secrets への追加も必要になります。

この記事で扱う課題は次の3つです。

  • 旧エンドポイント(app.rakuten.co.jp)から新エンドポイント(openapi.rakuten.co.jp/ichibams/api)への移行手順
  • fetch() で Referer が期待通りに届かなかったときの原因と対処法
  • GitHub Actions の cron ジョブを止めないためのキャッシュ戦略の変更

まず3行でまとめる — 変更点は2つだが、1つ見落としがちだった

楽天 Web Service の新エンドポイントは openapi.rakuten.co.jp/ichibams/api になりました。旧 app.rakuten.co.jp は2026年5月14日に完全停止する予定です。

変更点は2つあります。ベース URL の差し替えと、accessKey の追加です。旧 API は applicationId だけで認証できましたが、新 API では accessKey も必須になりました。(参考: 楽天 Ichiba Item Search API 公式ドキュメント

| 状況 | まずやること | | ----------------------------- | -------------------------------------------------------------------------------------- | | 移行が未完了 | ベース URL を openapi.rakuten.co.jp/ichibams/api に変更 + accessKey をヘッダで付与 | | 403 が返る | applicationId / accessKey / scope / 許可Webサイト / API バージョンを順番に確認 | | GitHub Actions で動かしている | Secrets に RAKUTEN_ACCESS_KEY を追加 + env: に明示 |

移行期間は2026年2月10日〜5月13日で設けられており、5月14日以降は旧ドメインへのリクエストが通らなくなります。(参考: 楽天 Web Service インフラ刷新告知


なぜ fetch() で Referer が届かなかったのか

403 の原因を掘り下げていくと、accessKey 未設定の他に「Referer が届いていない」疑惑が浮上しました。この点について整理します。

WHATWG Fetch Standard の forbidden request-header

まず仕様の話から入ります。WHATWG Fetch Standard では OriginRefererforbidden request-header として定義しています。ブラウザ環境ではこれらをスクリプトから手動で設定できず、ユーザーエージェントの管理下に置かれます。(参考: WHATWG Fetch Standard

undici は制限を解除している

undici(Node.js が採用している HTTP クライアント)は README の Specification Compliance セクションで、ブラウザ環境の forbidden header 制約を外し、ユーザーが制御できるようにしていると明記しています。(参考: nodejs/undici — GitHub

つまり undici.request() では RefererOrigin を手動で送信できます。

このプロジェクトでの実装判断

ただし、Node.js 組み込みの fetch()globalThis.fetch)と undici.request() は別物です。Node.js の fetch() は undici を内部で使いながらも、別のレイヤーで動作している部分があります。挙動は Node バージョンによって異なる可能性もあります。

このプロジェクトでは Node 22.x 環境での切り分けを踏まえ、undici.request() を採用しました。fetch() への依存をやめ、ヘッダをより細かく制御できる undici.request() に切り替えることで問題を解消しました。

403 の原因はヘッダだけではない

実際に 403 が返るケースは複数あります。ヘッダの問題に飛びつく前に次の順番で確認してください。

  1. applicationId が正しいか
  2. accessKey が設定されているか(新 API からの必須要件)
  3. 許可Webサイトに対象ドメインが登録されているか
  4. API バージョンが正しいか
  5. 楽天デベロッパーコンソールの利用規約への同意が完了しているか(筆者の切り分けポイント。公式の 403 原因一覧としては未確認)

コードを書く前に curl で疎通確認する

実装に入る前に、まず curl で認証情報と接続が正しく機能するか確認しておくと、原因の切り分けが楽になります。以下は現行バージョン(20260401)を使った確認用コマンドです。

curl -G "https://openapi.rakuten.co.jp/ichibams/api/IchibaItem/Search/20260401" \
  -H "accessKey: ${RAKUTEN_ACCESS_KEY}" \
  -H "Origin: https://your-site.example" \
  -H "Referer: https://your-site.example" \
  --data-urlencode "applicationId=${RAKUTEN_APPLICATION_ID}" \
  --data-urlencode "format=json" \
  --data-urlencode "keyword=テスト" \
  --data-urlencode "hits=1"

Origin の値はご自身のサイトドメインに変更してください。楽天デベロッパーコンソールの「許可Webサイト」に登録したドメインと一致している必要があります。

${RAKUTEN_ACCESS_KEY}${RAKUTEN_APPLICATION_ID} はシェルの環境変数から読み込まれます。事前に export RAKUTEN_ACCESS_KEY=... で設定するか、コマンドの先頭に RAKUTEN_ACCESS_KEY=... RAKUTEN_APPLICATION_ID=... を付けて実行してください。

レスポンスに "Items" 配列が返れば接続成功です。403 が返る場合は「なぜ fetch() で Referer が届かなかったのか」のセクションにある原因リストを参照してください。


実際のコードで見る移行前後の diff

エンドポイントとヘッダ設定の変更

src/lib/rakuten-api.ts を例に、変更箇所を示します。

変更前(fetch 版):

// ❌ 旧実装: fetch() + 旧エンドポイント
const endpoint = 'https://app.rakuten.co.jp/services/api/IchibaItem/Search/20170706'
const params = new URLSearchParams({
  applicationId: process.env.RAKUTEN_APPLICATION_ID || '',
  keyword: keyword.trim(),
  format: 'json',
  hits: '1',
})
const url = `${endpoint}?${params.toString()}`
const response = await fetch(url)
const data = (await response.json()) as RakutenSearchResponse

変更後(undici 版):

// ✅ 新実装: undici.request() + 新エンドポイント
import { request as undiciRequest } from 'undici'

// 認証情報は環境変数から取得(コードへの直書き厳禁)
const applicationId = process.env.RAKUTEN_APPLICATION_ID
if (!applicationId) {
  throw new Error('RAKUTEN_APPLICATION_ID is not configured')
}
const accessKey = process.env.RAKUTEN_ACCESS_KEY
if (!accessKey) {
  throw new Error('RAKUTEN_ACCESS_KEY is not configured')
}

const endpoint = 'https://openapi.rakuten.co.jp/ichibams/api/IchibaItem/Search/20220601'
const params = new URLSearchParams({
  applicationId: applicationId,
  keyword: keyword.trim(),
  format: 'json',
  hits: '1',
  // httpReferer はクエリパラメータにも追加(保守的な運用判断。公式必須要件ではない)
  httpReferer: 'https://your-site.example',
})
const url = `${endpoint}?${params.toString()}`

const { statusCode, body } = await undiciRequest(url, {
  method: 'GET',
  headers: {
    'User-Agent': 'your-app/1.0',
    Origin: 'https://your-site.example',
    Referer: 'https://your-site.example',
    accessKey: accessKey,
  },
})

if (statusCode < 200 || statusCode >= 300) {
  const errorText = await body.text()
  throw new Error(`Rakuten API error: ${statusCode} - ${errorText}`)
}

const data = (await body.json()) as RakutenSearchResponse

変更箇所は3か所になります。エンドポイント URL、accessKey ヘッダの追加、そして fetch() から undici.request() への切り替えです。

本実装では undici を直接依存として package.json に追加しました。未導入の場合は依存追加を検討してください。

API バージョンについて

現行実装では 20220601 を使用しています。楽天公式の現行バージョンは 20260401(2026年4月1日版)です。(参考: 楽天 Ichiba Item Search API 公式ドキュメント

20220601 が2026年5月14日の廃止対象かという点も確認しました。廃止対象バージョンは 20170706 以前に限定されており、20220601 はそのリストに含まれていません。(参考: 楽天市場APIの旧バージョン廃止のお知らせ)公式の旧バージョン一覧にも引き続き掲載されており、廃止告知はありません(2026年5月10日確認)。(参考: Rakuten Ichiba Item Search API version:2022-06-01

リスクを最小化したい場合は 20260401 への移行を検討してください。20220601 のまま継続することを妨げる公式告知は現時点では存在しません。


ここまでのポイント

  • ベース URL を openapi.rakuten.co.jp/ichibams/api に変更します
  • accessKey をヘッダ(accessKey: <値>)で付与します
  • Node 22.x 環境では fetch() から undici.request() に切り替える判断をしました
  • undici が未導入の場合は依存追加を検討します

GitHub Actions の cron を止めないための設定

このブログでは商品データの自動更新を daily-product-update.yml の cron ジョブで動かしています。CI/CD 側の設定変更も2点ありました。

Secrets の追加

まず RAKUTEN_ACCESS_KEY を GitHub Secrets に追加します。RAKUTEN_APPLICATION_ID は既存の Secrets にありますが、新 API から accessKey が必須になったため追加が必要です。

GitHub リポジトリの Settings → Secrets and variables → Actions → New repository secret から追加します。

Name:  RAKUTEN_ACCESS_KEY
Value: (楽天デベロッパーコンソールで取得した accessKey)

ワークフローの env: にも追記します。

- name: Update product data
  env:
    RAKUTEN_APPLICATION_ID: ${{ secrets.RAKUTEN_APPLICATION_ID }}
    RAKUTEN_ACCESS_KEY: ${{ secrets.RAKUTEN_ACCESS_KEY }} # ← 追加
    RAKUTEN_AFFILIATE_ID: ${{ secrets.RAKUTEN_AFFILIATE_ID }}

Cloudflare Workers 環境変数にも同様に RAKUTEN_ACCESS_KEY を追加しておくと、将来的なランタイム実行時にも対応できます。

キャッシュ戦略の変更

以前の構成には「キャッシュを削除してから更新する」ステップがありました。これが問題になることがあります。API 障害中にキャッシュ削除が先に走ると、既存の商品データが消えた後に更新も失敗するという最悪の状況になります。

actions/cache/restoreactions/cache/save を分離する方式に変更し、last-known-good のデータを保護するようにしました。

- name: Restore product cache
  uses: actions/cache/restore@v5
  with:
    path: src/data/product-cache.json
    key: product-cache-${{ github.run_id }}-${{ github.run_attempt }}
    restore-keys: |
      product-cache-

- name: Update product data
  env:
    RAKUTEN_ACCESS_KEY: ${{ secrets.RAKUTEN_ACCESS_KEY }}
    # ...

- name: Save product cache
  if: always() && hashFiles('src/data/product-cache.json') != ''
  uses: actions/cache/save@v5
  with:
    path: src/data/product-cache.json
    key: product-cache-${{ github.run_id }}-${{ github.run_attempt }}

if: always() を付けることで、更新ステップが失敗してもキャッシュの保存を試みます。hashFiles() はパターンに一致するファイルが存在しない場合に空文字を返すため、ファイルがない場合の無駄な保存を防ぐことができます(ファイルが存在するが中身が空・壊れている場合は別途チェックが必要です)。

restore-keys: product-cache- の prefix 指定により、直近のキャッシュから復元するフォールバックも機能します。API が一時的に落ちているときでも、前回の商品データでビルドを継続できます。


移行完了チェックリスト

コード側

  • [ ] ベース URL を openapi.rakuten.co.jp/ichibams/api に変更しました
  • [ ] applicationId をクエリに含め続けています
  • [ ] accessKey をヘッダ(accessKey: <値>)で付与しています
  • [ ] undici を依存に追加し undici.request() に切り替えました(または Node バージョン挙動を確認しました)
  • [ ] 旧ドメイン(app.rakuten.co.jp)への参照が残っていないか grep しました
grep -r "app.rakuten.co.jp" src/

CI/CD 側

  • [ ] GitHub Secrets に RAKUTEN_ACCESS_KEY を登録しました
  • [ ] daily-product-update.ymlenv:RAKUTEN_ACCESS_KEY を追記しました
  • [ ] キャッシュ戦略を restore / save 分離方式に変更しました

動作確認

  • [ ] 403 が解消したことを CI ログまたはローカル実行で確認しました
  • [ ] 商品データが正しく取得・更新されていることを確認しました

APIキーの管理(GitHub Secrets の運用・ローテーション・最小権限設計)については次の記事で詳しく解説しています。