Skip to content

Commit 8836dd4

Browse files
committed
add missing content-disposition header to rustdoc-json downloads
1 parent 32c418c commit 8836dd4

File tree

1 file changed

+113
-16
lines changed

1 file changed

+113
-16
lines changed

src/web/rustdoc.rs

Lines changed: 113 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -976,8 +976,11 @@ pub(crate) async fn json_download_handler(
976976
Some(wanted_compression),
977977
);
978978

979-
let mut response = match storage.get_raw_stream(&storage_path).await {
980-
Ok(file) => StreamingFile(file).into_response(if_none_match.as_deref()),
979+
let (mut response, updated_storage_path) = match storage.get_raw_stream(&storage_path).await {
980+
Ok(file) => (
981+
StreamingFile(file).into_response(if_none_match.as_deref()),
982+
None,
983+
),
981984
Err(err) if matches!(err.downcast_ref(), Some(crate::storage::PathNotFoundError)) => {
982985
// we have old files on the bucket where we stored zstd compressed files,
983986
// with content-encoding=zstd & just a `.json` file extension.
@@ -992,8 +995,11 @@ pub(crate) async fn json_download_handler(
992995
);
993996
// we have an old file with a `.json` extension,
994997
// redirect to that as fallback
995-
StreamingFile(storage.get_raw_stream(&storage_path).await?)
996-
.into_response(if_none_match.as_deref())
998+
(
999+
StreamingFile(storage.get_raw_stream(&storage_path).await?)
1000+
.into_response(if_none_match.as_deref()),
1001+
Some(storage_path),
1002+
)
9971003
} else {
9981004
return Err(AxumNope::ResourceNotFound);
9991005
}
@@ -1006,6 +1012,17 @@ pub(crate) async fn json_download_handler(
10061012
// Here we override it with the standard policy for build output.
10071013
response.extensions_mut().insert(CachePolicy::ForeverInCdn);
10081014

1015+
// set content-disposition to attachment to trigger download in browsers
1016+
// For the attachment filename we can use just the filename without the path,
1017+
// since that already contains all the info.
1018+
let storage_path = updated_storage_path.unwrap_or(storage_path);
1019+
let (_, filename) = storage_path.rsplit_once('/').unwrap_or(("", &storage_path));
1020+
response.headers_mut().insert(
1021+
CONTENT_DISPOSITION,
1022+
generate_content_disposition_header(filename)
1023+
.context("could not generate content-disposition header")?,
1024+
);
1025+
10091026
Ok(response)
10101027
}
10111028

@@ -3353,21 +3370,90 @@ mod test {
33533370
Ok(())
33543371
}
33553372

3356-
#[test_case("latest/json", CompressionAlgorithm::Zstd)]
3357-
#[test_case("latest/json.gz", CompressionAlgorithm::Gzip)]
3358-
#[test_case("0.1.0/json", CompressionAlgorithm::Zstd)]
3359-
#[test_case("latest/json/latest", CompressionAlgorithm::Zstd)]
3360-
#[test_case("latest/json/latest.gz", CompressionAlgorithm::Gzip)]
3361-
#[test_case("latest/json/42", CompressionAlgorithm::Zstd)]
3362-
#[test_case("latest/i686-pc-windows-msvc/json", CompressionAlgorithm::Zstd)]
3363-
#[test_case("latest/i686-pc-windows-msvc/json.gz", CompressionAlgorithm::Gzip)]
3364-
#[test_case("latest/i686-pc-windows-msvc/json/42", CompressionAlgorithm::Zstd)]
3365-
#[test_case("latest/i686-pc-windows-msvc/json/42.gz", CompressionAlgorithm::Gzip)]
3366-
#[test_case("latest/i686-pc-windows-msvc/json/42.zst", CompressionAlgorithm::Zstd)]
3373+
#[test_case(
3374+
"latest/json",
3375+
CompressionAlgorithm::Zstd,
3376+
"x86_64-unknown-linux-gnu",
3377+
"latest",
3378+
"0.2.0"
3379+
)]
3380+
#[test_case(
3381+
"latest/json.gz",
3382+
CompressionAlgorithm::Gzip,
3383+
"x86_64-unknown-linux-gnu",
3384+
"latest",
3385+
"0.2.0"
3386+
)]
3387+
#[test_case(
3388+
"0.1.0/json",
3389+
CompressionAlgorithm::Zstd,
3390+
"x86_64-unknown-linux-gnu",
3391+
"latest",
3392+
"0.1.0"
3393+
)]
3394+
#[test_case(
3395+
"latest/json/latest",
3396+
CompressionAlgorithm::Zstd,
3397+
"x86_64-unknown-linux-gnu",
3398+
"latest",
3399+
"0.2.0"
3400+
)]
3401+
#[test_case(
3402+
"latest/json/latest.gz",
3403+
CompressionAlgorithm::Gzip,
3404+
"x86_64-unknown-linux-gnu",
3405+
"latest",
3406+
"0.2.0"
3407+
)]
3408+
#[test_case(
3409+
"latest/json/42",
3410+
CompressionAlgorithm::Zstd,
3411+
"x86_64-unknown-linux-gnu",
3412+
"42",
3413+
"0.2.0"
3414+
)]
3415+
#[test_case(
3416+
"latest/i686-pc-windows-msvc/json",
3417+
CompressionAlgorithm::Zstd,
3418+
"i686-pc-windows-msvc",
3419+
"latest",
3420+
"0.2.0"
3421+
)]
3422+
#[test_case(
3423+
"latest/i686-pc-windows-msvc/json.gz",
3424+
CompressionAlgorithm::Gzip,
3425+
"i686-pc-windows-msvc",
3426+
"latest",
3427+
"0.2.0"
3428+
)]
3429+
#[test_case(
3430+
"latest/i686-pc-windows-msvc/json/42",
3431+
CompressionAlgorithm::Zstd,
3432+
"i686-pc-windows-msvc",
3433+
"42",
3434+
"0.2.0"
3435+
)]
3436+
#[test_case(
3437+
"latest/i686-pc-windows-msvc/json/42.gz",
3438+
CompressionAlgorithm::Gzip,
3439+
"i686-pc-windows-msvc",
3440+
"42",
3441+
"0.2.0"
3442+
)]
3443+
#[test_case(
3444+
"latest/i686-pc-windows-msvc/json/42.zst",
3445+
CompressionAlgorithm::Zstd,
3446+
"i686-pc-windows-msvc",
3447+
"42",
3448+
"0.2.0"
3449+
)]
33673450
#[tokio::test(flavor = "multi_thread")]
33683451
async fn json_download(
33693452
request_path_suffix: &str,
33703453
expected_compression: CompressionAlgorithm,
3454+
expected_target: &str,
3455+
expected_format_version: &str,
3456+
expected_version: &str,
33713457
) -> Result<()> {
33723458
let env = TestEnvironment::new().await?;
33733459

@@ -3397,6 +3483,13 @@ mod test {
33973483
let resp = web
33983484
.assert_success_cached(&path, CachePolicy::ForeverInCdn, env.config())
33993485
.await?;
3486+
assert_eq!(
3487+
resp.headers().get(CONTENT_DISPOSITION).unwrap(),
3488+
&format!(
3489+
"attachment; filename=\"dummy_{expected_version}_{expected_target}_{expected_format_version}.json.{}\"",
3490+
expected_compression.file_extension()
3491+
)
3492+
);
34003493
web.assert_conditional_get(&path, &resp).await?;
34013494

34023495
{
@@ -3469,6 +3562,10 @@ mod test {
34693562
let resp = web
34703563
.assert_success_cached(&path, CachePolicy::ForeverInCdn, env.config())
34713564
.await?;
3565+
assert_eq!(
3566+
resp.headers().get(CONTENT_DISPOSITION).unwrap(),
3567+
&format!("attachment; filename=\"{NAME}_{VERSION}_{TARGET}_latest.json\""),
3568+
);
34723569
web.assert_conditional_get(&path, &resp).await?;
34733570
Ok(())
34743571
}
@@ -3507,7 +3604,7 @@ mod test {
35073604
let response = web
35083605
.get(&format!("/crate/dummy/{request_path_suffix}"))
35093606
.await?;
3510-
3607+
assert!(response.headers().get(CONTENT_DISPOSITION).is_none());
35113608
assert_eq!(response.status(), StatusCode::NOT_FOUND);
35123609
Ok(())
35133610
}

0 commit comments

Comments
 (0)