期間を絞って 200 件弱のファイルを全選択して、ダウンロードを押しました。少し待つと、画面の上に赤い帯が出ます。
「要求されたファイルは使用できません。」
それだけ。ファイルは 1 つも落ちてきません。
先に結論
同じ画面に出くわした人向けに、対処だけ先に書きます。
- フォルダごとダウンロードすれば通ります。フォルダを 1 つ選んだ場合は件数に関係なく成功します
- 個別に選びたいなら 70 件くらいずつに分ける
- 権限の問題でもファイル破損でもありません。選んだ件数が多すぎるだけです
原因は Nextcloud 本体の実装で、GitHub には 2018 年から open のままの Issue があります(https://github.com/nextcloud/server/issues/7935 )。直す PR も出ていますが、draft のまま止まっています(https://github.com/nextcloud/server/pull/56543 )。
以下は、そこにたどり着くまでの話です。
最初は権限を疑った。それらしい理由があったので
このフォルダには、アクセス制御をわりと細かく効かせていました。グループ単位で拒否を掛けて、必要な人にだけ個別に許可を戻す、という作りです。
だから「要求されたファイルは使用できません」を見た瞬間、権限だと思いました。読めないファイルが混ざっていて、それを掴んだところで止まっている。ありそうな話です。
でも、おかしい。同じフォルダの中身は画面に一覧で出ています。サムネイルも出ている。読めているものを、読めないと言われている。
念のため数件だけ選んでダウンロードしてみたら、あっさり成功しました。ここで権限説はほぼ消えました。
サーバーのログに、何も無い
次はサーバー側です。時刻はスクリーンショットのファイル名で分かっていたので、その前後を見にいきました。
アプリケーションログ、何も無し。PHP のエラーログ、何も無し。Web サーバーのエラーログにいたっては、ファイルサイズが 0 バイトでした。
アクセスログも見ました。ダウンロードのリクエストが 1 件も記録されていません。プレビュー画像の取得と、通知のポーリングと、フォルダ一覧の取得。それだけ。
ここでしばらく手が止まりました。「エラーが出ていない」ならまだ調べようがあるのですが、「リクエストが来ていない」は打つ手が違ってきます。ブラウザ側で完結して失敗している、ということになるので。
念のため書いておくと、この「何も無い」が結果的に一番の手がかりでした。ただしその時点では、単に自分の探し方が悪いのだと思っていました。
ブラウザ側の実装を読んだら、全ファイル名が URL に入っていた
Nextcloud のフロントエンドはビルド済みの JS がサーバー上に置いてあります。読みにくいですが読めます。ダウンロード処理はこうなっていました。
// 複数選択時(変数名は minify 後のもの)
s.searchParams.append("accept", "zip");
s.searchParams.append("files", JSON.stringify(n)); // 選んだ全ファイル名を URL に載せる
await R(s.href);
// 送信部
async function R(e, s) {
await d.Ay.head(e); // 先に HEAD を投げる
const t = document.createElement("a");
t.download = s ?? ""; t.href = e; t.click();
}
選択したファイル名を JSON 配列にして、まるごと GET のクエリに載せています。そして実際にダウンロードを始める前に、同じ URL へ HEAD を 1 回投げる。この HEAD が失敗すると catch に落ちて、例のメッセージが出ます。
日本語訳のファイルも確認しました。原文は The requested files are not available. で、単数形の The requested file is not available. と訳文が同じです。だから「1 件が壊れているのか、全体が失敗したのか」も画面からは区別できません。
つまり件数が増えるほど URL が伸びる作りで、伸びすぎたら Web サーバーが門前払いする。ログに何も無かったのは、リクエストがアプリケーションまで届いていなかったからでした。
どこで切られるか測ってみる
推測で終わらせたくなかったので、長さだけを変えたリクエストを投げて閾値を探しました。存在しないパスに投げるので、実データには触りません。
for LEN in 7000 8000 8100 8200 8400; do
S=$(head -c $LEN /dev/zero | tr '\0' 'a')
curl -s -o /dev/null -w "$LEN -> %{http_code}\n" \
"https://example.com/nextcloud/remote.php/dav/files/dummy/?accept=zip&files=$S"
done
結果はきれいに割れました。
7000 -> 401
8000 -> 401
8100 -> 401
8200 -> 414
8400 -> 414
401 はアプリケーションまで届いた証拠(認証していないので当然です)。414 は Web サーバーが URL を長すぎると判断して返したものです。境界は 8,192 バイト付近。nginx のデフォルト値そのままでした(https://nginx.org/en/docs/http/ngx_http_core_module.html#large_client_header_buffers )。
そして実際に問題が起きたケースの URL 長を計算したら、2 万バイト前後ありました。上限の 2.5 倍です。ファイル名を JSON にすると引用符やカンマが %22 %2C にエンコードされて膨らむので、体感よりずっと長くなります。
1 件あたり 100 バイト強を消費する計算になるので、70 件から 80 件あたりが実用上の限界でした。
でも 414 なら、ログに残るはずでは
ここで矛盾に気づきます。414 を返したなら、アクセスログにその記録が残るはずです。でも実際のログには無かった。
理由はプロトコルの違いでした。curl は HTTP/1.1 で投げていて、ブラウザは HTTP/2 で繋いでいます。
PowerShell から HTTP/2 で同じ長さを投げてみました。
Invoke-WebRequest -Uri "https://example.com/...?files=$('b' * 9000)" -HttpVersion 2.0
# The HTTP/2 server closed the connection.
# HTTP/2 error code 'ENHANCE_YOUR_CALM' (0xb).
414 は返ってきません。接続そのものが切られます(RFC 9113 の定義: https://www.rfc-editor.org/rfc/rfc9113.html#section-7 )。
このときアクセスログに残るのは、こういう行です。
203.0.113.10 - - [07/Aug/2026:15:12:33 +0900] "-" 000 0 "-" "-"
リクエストラインの位置が -、ステータスが 000。何を要求されたのか記録できないまま切っているので、こうなります。
そして症状が出た時刻のログを見返したら、まったく同じ行が残っていました。最初に見たときは接続の切れ端だと思って読み飛ばしていた行です。
上の図のとおり、HTTP/1.1 なら 414 として素直に記録されるものが、HTTP/2 では「リクエストラインすら残らない 1 行」になります。ブラウザからの操作を調べるとき、ここを知らないと「リクエストが来ていない = クライアント側で完結して失敗した」と読んでしまいます。方向としては合っているのですが、実際にはサーバーに届いた上で、記録される前に拒否されている。この差は切り分けの手数に効きます。
上流は 8 年前から open のまま
原因が分かったので上流を確認しました。冒頭に貼った Issue #7935(https://github.com/nextcloud/server/issues/7935 )が 2018 年 1 月の起票で、今も open。コメントは 11 件ついています。
直す PR も見つかりました。#56543(https://github.com/nextcloud/server/pull/56543 )で、タイトルは「fix: Use POST instead of GET for multi-file downloads」。GET をやめて POST にする、というものです。方向は明らかに正しくて、これが入れば URL 長という制約自体が消えます。2025 年 11 月に出て、draft のまま止まっています。
回避策が存在する不具合は、優先度が上がりにくいのだろうと思います。フォルダごと落とせば動くので、致命ではないと判断される。ここは推測ですが、8 年という数字を見るとそう思えてきます。
Web サーバー側の上限を広げるべきか
いちばん手軽な対処は、Web サーバーが受け付けるリクエストラインの上限を広げることです。8k を 32k にすれば、今回の 200 件弱は通るようになります。
やめました。
理由は 2 つです。ひとつは、件数が増えれば同じ画面がまた出ること。今回のフォルダには全体で 1000 件近いファイルがあって、それを全部選ぶと URL は 10 万バイト規模になります。64k に広げても足りません。もうひとつは、「対応済み」という記憶だけが残ることです。次に同じ問い合わせが来たとき、上限を広げたはずなのに再発した、という状態から調べ直すことになります。
8k はどこにでもある普通の値で、それを踏み抜く URL を作っている側が原因です。そちらが直るまでは、フォルダごと落とす運用のほうが素直だと判断しました。
ついでに踏みかけた地雷
「期間で絞ったぶんだけ別フォルダに移して、それごと落とせばいい」という発想が出ました。理屈は通っています。
ただ、このフォルダは別に作っている連携の仕組みからも参照されていて、そちらはパス基準でファイルを引いていました。ファイル ID ではなく、保存したときのパスを記録して、そのパスで取りに行く作りです。
つまり移動した瞬間、連携側からは全部行方不明になります。移動先が同じフォルダの中でも同じです。パスが変われば当たらない。
結局この件は移動ではなくコピーで進めました。コピーなら元のパスが残るので、連携側は何事もなく動き続けます。落とし終わったらコピーを消すだけ。
これは Nextcloud に限った話ではなくて、ストレージを外部システムから参照している場合、そのキーがパスなのか ID なのかを知らないままファイルを動かすと壊れる、という一般的な話だと思います。今回はコードを読んでから動かしたので助かりました。
学んだこと
- 「ログに何も無い」は、それ自体が手がかり。エラーが出ていないのではなく、記録される前に弾かれているケースがある
- HTTP/2 では、上限超過が 414 ではなく接続切断になる。ブラウザの挙動を調べているのに curl(HTTP/1.1)で再現テストすると、症状が変わって混乱する
- エラーメッセージが原因を説明してくれるとは限らない。「要求されたファイルは使用できません」は権限やファイル破損を連想させますが、実際にはリクエストがサーバーに届いてすらいませんでした
- 回避策があるバグは、上流で長く放置されうる。踏んだ罠は自分のところで記録に残しておかないと、同じ調査を何度もやることになります
おわりに
今回いちばん時間を使ったのは、原因を突き止める部分ではなくて、「ログに何も無い」を自分の探し方の問題だと思い込んでいた時間でした。無いことに意味がある、と切り替えるまでが遠かった。
調査そのものは記録に残したので、次に同じ問い合わせが来たら「フォルダごと落としてください」の一言で終わります。それでいいと思っています。
小さく。踏んだ罠を書き残すことを、これからも続けていきます。
あわせて読みたい
- Nextcloud App Store に個人アプリを公開するときにハマったポイントまとめ。同じ Nextcloud まわりで、こちらは公開側の話です。上流の作法に合わせる大変さという意味では今回と地続きでした
※ ヘッダー画像とインフォグラフィックは AI(画像生成)で作成しています。
※ 本文の挿絵も AI(画像生成)で作成しています。
書いた人: ishizakahiroshi
群馬の北部で、保護猫2匹と暮らす、在宅エンジニア(何でも屋)
https://ishizakahiroshi.com/
https://github.com/ishizakahiroshi
X(業務委託・各種相談はこちら):
https://x.com/ishizakahiroshi
バックエンド・インフラ・AI連携まわりで、業務委託のご相談を受け付けています。フルリモートです。スポットや週2〜3時間からでも歓迎で、いろんな案件に携われたらうれしいです。こんな相談、歓迎です。



