「Chromeだと給与明細のPDFが落ちないのに、Edgeだと普通に落ちる」。自治体・官公庁ポータルや社内SaaSでよく起きるこの現象は、単なる“ブラウザの気分”ではなく、ヘッダー設定・混在コンテンツ・Cookie属性・Service Worker・企業ポリシーのいずれかが原因で再現します。本記事では再現条件の見分け方から実装の直し方、運用・テストまでを実務目線で体系的にまとめます。
問題の背景と整理(前提条件)
メキシコ政府職員向けポータル(に限らず、官公庁・金融・人事系ポータル全般)で「ChromeのみPDFダウンロードが停止/開始しない」「Edgeでは成功する」という問い合わせが増えています。Chromium系であるEdgeと挙動差が出る場合、多くは以下のいずれかです。
- HTTPSページからHTTPリソースを落とそうとして 混在コンテンツ としてブロックされている。
- レスポンスの HTTPヘッダー(Content-Type, Content-Disposition, Cache-Control など) が不足・不整合。
- SameSite 属性を含む認証Cookieの扱いがChromeとEdgeで異なり、リダイレクトやファイル取得がループ・空振りする。
- ポータルがPWA化され Service Worker が fetch を横取りし、Blob/Streamの返し方がChromeでだけ詰まる。
- 企業の セキュリティポリシー/拡張機能/プロキシ がChromeだけ厳格に適用され、ダウンロードが抑止されている。
- 50MB超などの 大容量PDF を一括送信して Network Error(タイムアウト・切断・メモリ)を誘発。
受け入れられた回答として「Chrome側の問題なので公式ヘルプやコミュニティを参照」とされるケースは多いものの、実務では上記の技術的ポイントを順に潰すことで Chromeでも確実にダウンロードできる実装 に着地できます。
先に結論(意思決定の指針)
- 短期:業務停止を避けるため、問題ユーザには Microsoft Edgeの利用案内 を出す。
- 中期:以下のチェックリストで原因を特定し、Chromeでも落ちる実装へ修正。
- 長期:E2E自動テストを導入し、混在コンテンツやCookie属性変更に伴う将来の破綻を検知。
最初に見るチェックリスト(即効性の高い順)
| 対策カテゴリ | 具体的な確認ポイント/推奨設定 |
|---|---|
| HTTPレスポンスヘッダー | Content-Type: application/pdf を明示。 Content-Disposition: attachment; filename="xxx.pdf" を付与(「ダウンロード」意図を明確化)。 Content-Length を正確に返す(プロキシやクライアントの進捗制御に重要)。 Cache-Control を適切化(public, max-age=86400 等で再取得を高速化しタイムアウトを減らす)。 |
| 混在コンテンツ(Mixed Content) | ページが https:// なのにPDFリンクが http:// だとChromeは既定でブロック。リンク・リダイレクト先・CDNを完全HTTPS化。 |
| Chrome設定 & 拡張機能 | 「安全でないダウンロードをブロック」を確認(企業ポリシーで強制されている場合あり)。 広告ブロック/セキュリティ系拡張を一時停止して再検証。 シークレットウィンドウ+拡張無効で再現性を比較。 |
| キャッシュ & バージョン | ブラウザキャッシュ/Cookieを削除し、最新版Chromeで再試行。旧版(特に古いPDFビューア)は挙動差が出やすい。 |
| Service Worker | PWAの場合、fetch の respondWith が response.blob() を正しく返しているか。Opaqueレスポンスやストリーム中断で空振り化しやすい。 |
| SameSite Cookie | ファイル配信が別ドメイン/CDNの場合、認証Cookieに SameSite=None; Secure が必要。無いとリダイレクトで詰まり、Edgeのみ成功することがある。 |
| iframe/sandbox | ダウンロードを <iframe sandbox> 経由で開始しているとChromeは抑止。allow-downloads(必要に応じ allow-downloads-without-user-activation)を付ける。 |
| ユーザー操作(User Activation) | プログラム起点の自動ダウンロードは抑止。必ず「クリック」等のユーザー操作で <a download> を起動。 |
| ファイルサイズ・サーバ構成 | 50MB超のPDFを単発送信するとネットワーク切断・タイムアウトが増える。HTTP/2なCDN経由、Range 対応、送出のチャンク最適化で安定化。 |
| デバッグ | Chrome DevTools → NetworkでPDFリクエストを確認。ステータスコード・Blocked Reason・Initiator・リダイレクト鎖を特定。 |
症状別:切り分けの作法
| 症状 | 観測ポイント(DevTools) | よくある原因 | 対処 |
|---|---|---|---|
| クリックしても無反応 | Networkにリクエスト自体が出ない/Consoleに「download blocked」等 | ユーザー操作なしの自動開始/sandboxed iframe/ポップアップ抑止 | <a download> をクリックで起動、allow-downloads 付与、ポップアップ/ダウンロード許可をサイト設定でON |
| 「Blocked:mixed-content」 | Networkの Blocked Reason に mixed-content | HTTPS→HTTPへのダウンロード | リソース全体をHTTPSに統一、CDN/リダイレクトの完全HTTPS化 |
| 302→200を繰り返しダウンロード開始せず | リダイレクト鎖でCookie未送信/未設定 | SameSite属性不整合/サブドメイン跨ぎ | Cookieを SameSite=None; Secure に、または署名付与のダウンロードトークン方式に変更 |
| PDFビューワが白画面で止まる | Responseヘッダーの Content-Type 未設定/nosniff との衝突 | ヘッダー不足や誤ったMIME | Content-Type: application/pdf を必ず付与、Content-Disposition: attachment にしてビューワを経由させない |
| 大きいPDFのみ失敗 | 途中までダウンロード→切断 | プロキシ・SSL検査・モバイル回線・Keep-Alive切れ | CDN経由・Range対応・Content-Length 正確化・Cache-Control 最適化 |
Chrome固有のブロック条件と回避の勘所
混在コンテンツ(HTTPSページからHTTPダウンロード)
Chromeは段階的強化により、HTTPSページからのHTTPダウンロードを広範囲でブロックします。PDFや実行ファイルは特に対象。完全HTTPS化が唯一の本質解です。サブリソース・リダイレクト先・署名URL・CDNのオリジンまで念入りに確認します。
SameSite Cookie とサブドメインまたぎ
認証が必要なPDF配信で cdn.example.com のようにサブドメインを分けると、Chromeは SameSite 既定値の違いでCookieを送らず、Edgeだけ成功するケースが出ます。クロスサイトでCookieを運ぶ場合は SameSite=None; Secure を必ず指定します。加えて、ダウンロードだけは 署名付き一時URL(JWTやHMAC署名)でCookie非依存にすると堅牢です。
iframe sandbox / ユーザー操作の要件
Chromeは自動ダウンロードや <iframe sandbox> からのダウンロードを厳格に抑止します。ダウンロードはユーザーのクリックで開始し、iframeを使うなら sandbox に allow-downloads を付与。どうしても事前にバックグラウンド取得したい場合は、fetch→Blob→URL.createObjectURL()→<a download>.click() の王道パターンに切り替えます。
Safe Browsing/企業ポリシー・拡張機能
企業管理のChromeは、グループポリシーやセキュリティ拡張でダウンロードが制限されることがあります。シークレットウィンドウ+拡張無効で再現しない場合、ポリシーが原因です。情シス向けには「特定ドメインのダウンロード許可」「拡張機能の除外」ルール追加を依頼します。
大容量PDFとネットワーク経路
50MB超のPDFは、モバイル回線・SSL検査プロキシ・一部ルータのバッファで失敗が目立ちます。Accept-Ranges: bytes による Range配信、HTTP/2の多重化、Content-Length の厳密化、Cache-Control による再取得短縮で成功率が上がります。
サーバ実装の具体例(すぐ貼れるスニペット)
Nginx(静的またはリバースプロキシ)
location /payslips/ {
# MIMEの明示
types { application/pdf pdf; }
default_type application/pdf;
# ダウンロード意図
add_header Content-Disposition 'attachment; filename="$arg_name"';
add_header X-Content-Type-Options nosniff;
add_header Cache-Control 'public, max-age=86400';
# Range配信(デフォルトでOKだが念のため)
# 受け側アプリにプロキシする場合
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
Apache(.htaccess)
<FilesMatch "\.pdf$">
ForceType application/pdf
Header set Content-Disposition "attachment"
Header set X-Content-Type-Options "nosniff"
Header set Cache-Control "public, max-age=86400"
</FilesMatch>
IIS(web.config)
<configuration>
<system.webServer>
<staticContent>
<mimeMap fileExtension=".pdf" mimeType="application/pdf" />
</staticContent>
<httpProtocol>
<customHeaders>
<add name="Content-Disposition" value="attachment" />
<add name="X-Content-Type-Options" value="nosniff" />
<add name="Cache-Control" value="public, max-age=86400" />
</customHeaders>
</httpProtocol>
</system.webServer>
</configuration>
Node.js(Express)
app.get('/payslip/:id', async (req, res) => {
const filePath = await resolvePayslipPath(req.params.id, req.user.id);
const safeName = `payslip-${req.params.id}.pdf`;
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="${safeName}"`,
'X-Content-Type-Options': 'nosniff',
'Cache-Control': 'public, max-age=86400'
});
res.sendFile(filePath);
});
Java(Servlet)
@WebServlet("/payslip/*")
public class PayslipServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
Path path = findPdf(req);
resp.setContentType("application/pdf");
resp.setHeader("Content-Disposition", "attachment; filename=\"payslip.pdf\"");
resp.setHeader("X-Content-Type-Options", "nosniff");
resp.setHeader("Cache-Control", "public, max-age=86400");
Files.copy(path, resp.getOutputStream());
}
}
PHP
<?php
$path = resolve_pdf_path($_GET['id'], $user_id);
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="payslip.pdf"');
header('X-Content-Type-Options: nosniff');
header('Cache-Control: public, max-age=86400');
readfile($path);
exit;
Cookie/SameSite を確実に通す設定例
ダウンロードエンドポイントが別オリジンの場合、認証Cookieは SameSite=None; Secure が必須です。代理配信(Nginx)の場合の例:
# 認証Cookie名 sessionid を例示
proxy_cookie_flags sessionid SameSite=None secure httponly;
proxy_cookie_domain backend.example.gov portal.example.gov;
.NETやJavaでサーバ側から付与する場合も、同等の設定を適用してください。
フロントエンド実装:Chromeに嫌われないダウンロード
最小構成(ユーザー操作で開始)
// HTML
<a id="dl" href="/payslip/2024-01?token=..." download>PDFをダウンロード</a>
事前取得(fetch→Blob→ObjectURL→download)
async function downloadPdf(url, suggestedName = 'payslip.pdf') {
const res = await fetch(url, { credentials: 'include' });
if (!res.ok) throw new Error('fetch failed');
const blob = await res.blob();
const objUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = objUrl;
a.download = suggestedName;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(objUrl);
}
この方式はユーザーのクリックイベントハンドラ内で呼び出してください。自動実行するとChromeは抑止します。
Service Worker(PWA)での落とし穴と正解例
よくある失敗
event.respondWith(fetch(event.request))せずに独自ストリームを返し、途中でGCされて中断。mode: "no-cors"で opaque 応答を返し、Blob化に失敗。- 資格情報付き(
credentials: "include")でのリダイレクトを誤処理。
堅実なパターン
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (url.pathname.startsWith('/payslip/')) {
event.respondWith((async () => {
// 資格情報を含めてバックエンドへ(Cookie必須なら)
const res = await fetch(event.request, { credentials: 'include' });
// 必要ならヘッダーを調整して返す
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: new Headers([
['Content-Type', 'application/pdf'],
// ダウンロードを明確に
['Content-Disposition', 'attachment; filename="payslip.pdf"'],
['Cache-Control', 'public, max-age=86400'],
['X-Content-Type-Options', 'nosniff'],
]),
});
})());
}
});
DevToolsでの診断ポイント
- Networkタブ:PDFの行を選択し、Headers→Response Headers で
Content-TypeとContent-Dispositionを確認。 - Initiator:Other/download/iframe 等から発火しているか。
iframe sandboxが見えたら許可属性を付ける。 - Blocked Reason:mixed-content, opaque response, other などの値を特定。
- リダイレクト:
200 → 302 → 302 → 200のような鎖でCookieが落ちていないときはSameSiteを疑う。 - Size/Time:大容量のみ失敗する場合はネットワーク経路・プロキシ・CDN最適化を検討。
企業・官公庁環境での運用Tips(Edge案内を含む)
- 暫定運用:根本原因が未確定の間は、通知やヘルプに 「Chromeで失敗時はEdgeをお試しください」 を明記し、業務を止めない。
- サイト設定の案内:Chromeのアドレスバー右端アイコン(盾や鍵)から、該当サイトの 「ダウンロード」 を許可する手順を図解で掲載。
- ポリシー連携:情シスへは「対象ドメインのダウンロード許可」「特定拡張の除外」を依頼できるテンプレート文を用意。
パフォーマンス最適化(失敗率の低減)
Cache-Control: public, max-age=86400で再ダウンロードを軽量化。ETag/Last-Modifiedによる条件付き取得を有効化。- CDNでHTTP/2/3を有効にし、エッジから配信。
Accept-Ranges: bytesを返し、途中再開を可能に。- 圧縮はPDFでは一般に効果が薄いが、ヘッダー圧縮(HPACK/QPACK)は恩恵がある。
自動テスト(壊れにくい運用へ)
Playwright(Chrome/Edge/Firefox横断)
import { test, expect } from '@playwright/test';
test('PDFがダウンロードできる', async ({ browser }) => {
const context = await browser.newContext({ acceptDownloads: true });
const page = await context.newPage();
await page.goto('[https://portal.example.gov/login](https://portal.example.gov/login)');
await page.getByLabel('職員番号').fill('123456');
await page.getByLabel('パスワード').fill('••••••');
await page.getByRole('button', { name: 'ログイン' }).click();
const [ download ] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('link', { name: /給与明細.*PDF/ }).click()
]);
const path = await download.path();
expect(path).not.toBeNull();
});
Cypress
describe('PDFダウンロード', () => {
it('給与明細PDFが取得できる', () => {
cy.login(); // 自前のカスタムコマンド
cy.contains('PDF').click();
cy.verifyDownload('payslip', { timeout: 60000 }); // cypress-downloadfile等のプラグイン
});
});
よくある質問(FAQ)
Q. Edgeなら成功するのはなぜ?
A. エンジンは近いものの、企業ポリシーや拡張の適用、ダウンロード抑止の閾値、Cookie既定値の違いなどで挙動差が出ます。アプリ側でChromeに合わせておくと、将来の仕様変更にも強くなります。
Q. Content-Disposition: inline ではダメ?
A. ChromeのPDFビューアに任せる場合はinlineでも表示されますが、ビューワ側やCSPの影響で白画面化する事例があるため、確実にファイルとして落とすなら attachment が安全です。
Q. window.open() で開いて保存させるのは?
A. ポップアップブロックやユーザー操作要件に引っかかることがあり、安定しません。<a download> または fetch→Blob→ObjectURL 方式を推奨します。
Q. CDN越しでCookieが渡らない
A. オリジンと異なる場合は SameSite=None; Secure が必須です。可能ならダウンロードは時間制限付き署名URLとし、Cookie依存を脱却しましょう。
実務テンプレート(チェック完了報告の例)
【案件】ChromeでPDFが落ちない(Edge可)
【結論】Mixed Content+Cookie属性が原因。完全HTTPS化&CookieをSameSite=None; Secureに修正して解消。
【施策】
1) PDF配信ドメインをhttps化、リダイレクト先も統一
2) Content-Type/Disposition/Length/Cache-Controlを明示
3) iframeのallow-downloadsを付与、クリック起点へ変更
4) 50MB超はRange配信、CDN経由化
【運用】暫定でEdge案内、ヘルプに許可手順を追記
【テスト】PlaywrightでChrome/Edge/FirefoxのE2EをCIに追加
まとめ
「ChromeだけPDFが落ちない」は、ヘッダー・混在コンテンツ・Cookie・Service Worker・ポリシー・大容量のいずれかに収束します。受け入れられた簡易回答に留まらず、本記事のチェックリストとスニペットを当てはめれば、Edge依存から脱却し、Chromeを含む全主要ブラウザで安定配布が可能です。まずはHTTPS統一とContent-Disposition明示、そして SameSite=None; Secure とユーザー操作起点のダウンロードに正す――この順で取り組めば最短で解決に近づけます。

コメント