Skip to content

fix(classroom): Google Classroom 共有失敗の本当の原因を表示する (#1175) - #1183

Merged
takaokouji merged 2 commits into
developfrom
topic/autopilot-1175
Oct 10, 2026
Merged

takaokouji merged 2 commits into
developfrom
topic/autopilot-1175

Conversation

@smalruby3-editor-bot

Copy link
Copy Markdown

Summary

課題の「Google Classroom に共有」で Google Classroom API error としか出ず、原因が
誰にも(先生にも CloudWatch にも)分からない状態を解消する。

Lambda の Google Classroom プロキシは、401 / 403 以外のすべての失敗を一律 502
「Google Classroom API error」に潰し
、Google が返したエラー本文を捨てていた。
今回の報告はまさにこの分岐(= Google は 401/403 以外を返している)。

このPRで:

  • Google の error.status / error.message をパースして レスポンスとログに透過
    (アクセストークンは記録しない)。UI 翻訳用の安定キー reason を付与。
    • 404 → googleCourseNotFound(コース削除済み / そのコースの先生ではない)
    • 400 → googleInvalidRequest(FAILED_PRECONDITION 等を含む)
    • 401 / 403 → 従来どおり(googleTokenExpired / googlePermissionDenied)
    • その他 → 502 のまま。ただし Google の status/message は同梱
  • UI は reason ごとに 日本語の対処可能なメッセージを表示(生の英語文字列を出さない)。
    Google の 401 を「クラスのセッション切れ」と誤訳していた分岐も解消。
  • 同意画面でスコープを一部だけ許可できてしまう問題に対処。課題作成に必要な
    classroom.coursework.students を hasGrantedAllScopes で検証し、不足していれば
    prompt: 'consent' で再要求。403 ではキャッシュ済みトークンを破棄して次回再認可する。

原因について(重要・未確定)

prod のログ / AWS 認証にアクセスできないため、Google が返している具体的なステータスは
特定できていない
。確実に言えるのは「401 でも 403 でもない」こと(UI の文言から逆算)。
有力候補は 404(ひもづけ先コースが消えた / 共同管理者がその Google Classroom コースの
先生ではない)と 400 系。

本PRは その切り分けを可能にし、かつ有力候補それぞれを先生が自力で直せるメッセージに
する
ところまでを対象にしている。次に同じ報告が来た時点で(またはこの変更を stg/prod に
反映後に再現してもらえば)googleStatus / googleMessage から一発で確定できる。
確定後に追加の修正が要る場合は別 Issue で扱う。

Changes Made

  • infra/smalruby-classroom/lambda/handler.ts
    • parseGoogleApiErrorBody() / googleApiErrorResponse() を純粋関数として追加・export
    • callGoogleClassroomAPI が失敗時に本文をパースし CloudWatch に記録(トークンは出さない)
    • GoogleAPIError に googleStatus / googleMessage を保持
  • packages/scratch-gui/src/containers/classroom-error-utils.js — reason ごとの翻訳を追加
  • packages/scratch-gui/src/lib/google-classroom-auth.js — 付与スコープの検証 + 再同意
  • packages/scratch-gui/src/containers/use-google-classroom.js — 403 でもトークン破棄
  • src/locales/ja.js / ja-Hira.js — 新メッセージ 5 件(対で追加)
  • docs/classroom/architecture.md — GC プロキシのエラーレスポンス仕様を追記

Test Coverage

  • infra/smalruby-classroom/lambda/tests/handler-google-error.test.ts(新規 13 件)
    — パーサの異常系(HTML / 非 JSON / 型違い / 長文切り詰め)とステータス写像
  • packages/scratch-gui/test/unit/lib/google-classroom-auth.test.js(新規 6 件)
    — 全許可 / 一部許可→再同意 / それでも不足 / キャッシュしない / 旧 GIS 互換
  • packages/scratch-gui/test/unit/containers/classroom-error-utils.test.js(+7 件)

実行結果: infra 全テスト 328 passed / scratch-gui 該当 2 ファイル 21 passed /
npm run lint(eslint --max-warnings 0 + prettier)クリーン。

デプロイ

Lambda 側の変更を含むため、stg / prod への反映は人間が実施(worker は cdk deploy
しない)。stg で試す場合は PR に deploy-stg ラベルを付ける。

Related Issues

Closes #1175

…g an assignment

Posting an assignment collapsed every non-401/403 Google failure into a bare 502
"Google Classroom API error", so a teacher (and CloudWatch) learned nothing about
why the share failed.

Google's error.status/message are now parsed, logged (never the access token) and
passed through with a stable reason key, and the UI translates each reason into an
actionable Japanese message instead of echoing the raw server string.

The consent screen also lets a teacher untick classroom.coursework.students, which
is required to create course work; the token is now checked with hasGrantedAllScopes
and re-requested with prompt=consent, and a 403 drops the cached token.

Refs #1175

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@smalruby3-editor-bot smalruby3-editor-bot Bot added the 🤖 autopilot autopilot が処理対象にした Issue/PR(daemon が付与) label Oct 10, 2026
@smalruby3-editor-bot

smalruby3-editor-bot Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Author

🤖 autopilot status

field value
Status Review
AI Status —
HITL Yes
Size —

Linked issue #1175. Maintained by autopilot (single writer); do not edit.

@github-actions

Copy link
Copy Markdown

🚀 Preview deployed: https://smalruby.jp/smalruby3-editor/topic/autopilot-1175/

再同意のポップアップは最初のクリックから時間が経ってから開くため、ブラウザに
ブロックされたり先生が閉じたりしうる。その拒否を googlePermissionDenied として
扱い、GIS の生の英語エラーが UI に出るのを防ぐ。state 不一致 (CSRF の疑い) は
この扱いから除外する。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@smalruby3-editor-bot

Copy link
Copy Markdown
Author

[Must] 再同意(prompt: 'consent')が失敗したときに、生の英語エラーが UI に出る — このPRが消そうとしている「不透明なメッセージ」そのもの(修正済み: 491b156)

requestClassroomAccessToken() は、部分許可を検出すると _requestToken('consent') で再要求する。
しかしこの 2 回目のポップアップは、最初のクリック(ユーザー操作)から数秒〜数十秒経ってから開く
ため、ブラウザの transient activation が切れており ポップアップブロックされる可能性が高い。
その場合 GIS は response.error = 'popup_failed_to_open' でコールバックし、_requestToken は
Error('Google Classroom authentication failed: popup_failed_to_open') で reject する。
先生がその同意画面を閉じた / 拒否した場合(access_denied)も同じ経路。

このエラーには googleReason が付いていないため translateGoogleClassroomError() は null を返し、
translateError() の最後の return msg に落ちて 英語の生文字列がそのまま先生に表示される。
gui.classroom.error.googlePermissionDenied を用意したのに、一番起きやすい分岐で使われない。

修正内容:

  • 再同意の reject を捕捉し、googleReason = 'googlePermissionDenied' を付けたエラーに変換
    (元エラーは cause に保持)。
  • ただし OAuth state 不一致(CSRF の疑い)は downgrade しない。_requestToken の state 不一致
    エラーに isStateMismatch フラグを付け、その場合はそのまま再 throw する。
  • テスト 2 件追加: popup_failed_to_open → googlePermissionDenied / state 不一致 → そのまま。
  • docs/classroom/architecture.md の仕様記述も追従。

@smalruby3-editor-bot

Copy link
Copy Markdown
Author

[Question] 全スコープ必須化により、コース一覧・インポートまで巻き添えでブロックされる(人間の判断に委ねる)

requestClassroomAccessToken() は handlePostAssignment(課題投稿)だけでなく
handleLoadGoogleCourses(コース一覧 → handleCreateClassFromCourse でのインポート)からも呼ばれる。

  • 変更前: 先生が classroom.coursework.students のチェックを外しても、読み取り系 2 スコープの
    トークンがキャッシュされ、コース一覧・インポートは動いた(課題投稿だけが Google 内で失敗した)。
  • 変更後: _hasAllScopes が false → 再同意 → それでも不足なら throw。つまり
    コース一覧もインポートも一切できなくなる。

「投稿に必要なスコープを最初から確実に取る」という意図は理解できるし、オール・オア・ナッシングの方が
状態が単純という利点もある。一方で、課題投稿を使うつもりのない先生のクラス作成までブロックするのは
退行です。

選択肢:

  1. 現状維持(全スコープ必須)。シンプルだが上記の退行を許容する。
  2. 投稿フロー(handlePostAssignment)でのみ全スコープを要求し、一覧/インポートは
    CLASSROOM_SCOPE_LIST の読み取り 2 件だけ検証する(requestClassroomAccessToken(requiredScopes) の形)。

改修コストは 2 の方が高く(呼び出し側の分岐 + トークンキャッシュを「どのスコープを持つか」付きで
持つ必要がある)、どちらが望ましいかは運用上の判断なので、自分では直さずここに残します。

@smalruby3-editor-bot

Copy link
Copy Markdown
Author

[Question] 新メッセージが「配信(投稿)」前提の文言だが、コース一覧・インポートの失敗でも出る(人間の判断に委ねる)

translateError() は共通関数で、handleLoadGoogleCourses(コース一覧)と
handlePostAssignment(課題投稿)の両方から呼ばれる。新しい 5 件は投稿を前提にした文言になっている:

id 文言 一覧/インポート時に出ると
googlePermissionDenied 「…に課題を配信する権限がありません」 一覧取得の 403 でも「配信の権限」と言ってしまう
googleApiFailed / googleApiFailedDetail 「Google Classroom に配信できませんでした」 一覧取得の失敗でも「配信できませんでした」になる

英語の生文字列よりは明確に改善しているので退行ではありませんが、先生が「配信してないのに?」と
混乱しうる。translateError(intl, err, context) の context は既にあるので
(例: context: 'googleList')分岐はできますが、ja / ja-Hira 両方にメッセージが倍増するコストが
かかります。改修コスト > 効果と判断し、自分では直さずここに残します。

@smalruby3-editor-bot

Copy link
Copy Markdown
Author

[Question] ドキュメント DoD の取りこぼし(修正済み: 491b156)

.claude/rules/documentation.md の「開発ワークフローの DoD」との照合で 2 点:

  1. docs/classroom/architecture.md の追記文「先生に表示するアクセストークンは classroom.coursework.students
    を含む 3 スコープを要求するが…」が文として壊れている(トークンを先生に表示するわけではない)。
    → 「クライアントが取得するアクセストークンは…」に修正し、あわせて再同意がブロック/拒否された
    ときの扱い(Must の修正分)と state 不一致の除外も明記。
  2. docs/classroom/source-code.md の Lambda 関数一覧に callGoogleClassroomAPI() は載っているのに、
    同じファイルに新規 export した parseGoogleApiErrorBody() / googleApiErrorResponse() が未記載。
    → 2 行追加。

どちらも機械的な追従なのでこちらで直しました。

@smalruby3-editor-bot

Copy link
Copy Markdown
Author

[FYI] console.error が Google 401(日常的なトークン失効)でも出る

callGoogleClassroomAPI は失敗時に一律 console.error('Google Classroom API error', {...}) を出す。
401 は「アクセストークンの有効期限切れ」という正常系に近い事象で、クライアントは
clearClassroomAccessToken() して再認可するだけなので、CloudWatch の ERROR がノイズになる。
401 は console.warn、それ以外を console.error に分けると、本当に調査が要る失敗が見つけやすい。
今回の目的(原因の可視化)は達成できているので、対応不要と判断しています。

@smalruby3-editor-bot

Copy link
Copy Markdown
Author

[FYI] .prettierignore の追加位置がアルファベット順から外れている

packages/scratch-gui/.prettierignore の !test/unit/lib/* ホワイトリストは
classroom-assignment-utils → classroom-download-utils → classroom-group-utils →
classroom-retention … と並んでいるが、!test/unit/lib/google-classroom-auth.test.js が
classroom-group-utils と classroom-retention の間に挿入されている。
機能には影響しないが、次に追加する人が並び順を見失うので、g の位置へ移すかこのままにするか。
対応不要と判断しています(Prettier 対象にはなっており、npm run lint はクリーン)。

@smalruby3-editor-bot

Copy link
Copy Markdown
Author

🤖 敵対的レビュー完了。指摘 6 件(Must 1 / Question 3 / FYI 2)。Must は修正済み(491b156)。Question のうちドキュメント追従(#4)も修正済み、スコープ必須化の影響範囲(#2)とメッセージ文言の文脈(#3)は設計判断のため人間レビューに委ねます。

@smalruby3-editor-bot smalruby3-editor-bot Bot added the 🙋 HITL 人間の対応待ち(Project HITL=Yes の投影) label Oct 10, 2026
@smalruby3-editor-bot
smalruby3-editor-bot Bot marked this pull request as ready for review October 10, 2026 02:56
@takaokouji
takaokouji merged commit be965ae into develop Oct 10, 2026
33 checks passed
@takaokouji
takaokouji deleted the topic/autopilot-1175 branch October 10, 2026 04:50
github-actions Bot pushed a commit that referenced this pull request Oct 10, 2026
…lot-1175

fix(classroom): Google Classroom 共有失敗の本当の原因を表示する (#1175)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🤖 autopilot autopilot が処理対象にした Issue/PR(daemon が付与) 🙋 HITL 人間の対応待ち(Project HITL=Yes の投影)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

作成した課題がClassroomAPI errorによりgoogle classroomに共有できない。

1 participant