ChromeでPDFをダウンロードできない時の原因と対処法|Edgeでは成功するケースの根本解説(混在コンテンツ・SameSite・Service Workerまで)

「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 WorkerPWAの場合、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-contentHTTPS→HTTPへのダウンロードリソース全体をHTTPSに統一、CDN/リダイレクトの完全HTTPS化
302→200を繰り返しダウンロード開始せずリダイレクト鎖でCookie未送信/未設定SameSite属性不整合/サブドメイン跨ぎCookieを SameSite=None; Secure に、または署名付与のダウンロードトークン方式に変更
PDFビューワが白画面で止まるResponseヘッダーの Content-Type 未設定/nosniff との衝突ヘッダー不足や誤ったMIMEContent-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)

&lt;FilesMatch "\.pdf$"&gt;
  ForceType application/pdf
  Header set Content-Disposition "attachment"
  Header set X-Content-Type-Options "nosniff"
  Header set Cache-Control "public, max-age=86400"
&lt;/FilesMatch&gt;

IIS(web.config)

&lt;configuration&gt;
  &lt;system.webServer&gt;
    &lt;staticContent&gt;
      &lt;mimeMap fileExtension=".pdf" mimeType="application/pdf" /&gt;
    &lt;/staticContent&gt;
    &lt;httpProtocol&gt;
      &lt;customHeaders&gt;
        &lt;add name="Content-Disposition" value="attachment" /&gt;
        &lt;add name="X-Content-Type-Options" value="nosniff" /&gt;
        &lt;add name="Cache-Control" value="public, max-age=86400" /&gt;
      &lt;/customHeaders&gt;
    &lt;/httpProtocol&gt;
  &lt;/system.webServer&gt;
&lt;/configuration&gt;

Node.js(Express)

app.get('/payslip/:id', async (req, res) =&gt; {
  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

&lt;?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
&lt;a id="dl" href="/payslip/2024-01?token=..." download&gt;PDFをダウンロード&lt;/a&gt;

事前取得(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) =&gt; {
  const url = new URL(event.request.url);
  if (url.pathname.startsWith('/payslip/')) {
    event.respondWith((async () =&gt; {
      // 資格情報を含めてバックエンドへ(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での診断ポイント

  1. Networkタブ:PDFの行を選択し、Headers→Response Headers で Content-Type と Content-Disposition を確認。
  2. Initiator:Other/download/iframe 等から発火しているか。iframe sandbox が見えたら許可属性を付ける。
  3. Blocked Reason:mixed-content, opaque response, other などの値を特定。
  4. リダイレクト:200 → 302 → 302 → 200 のような鎖でCookieが落ちていないときは SameSite を疑う。
  5. 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ダウンロード', () =&gt; {
  it('給与明細PDFが取得できる', () =&gt; {
    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 とユーザー操作起点のダウンロードに正す――この順で取り組めば最短で解決に近づけます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次