Skip to content

Commit f9afef4

Browse files
authored
fix(http): improve missing-@ upload errors (#19752)
fix(http): improve missing-@ upload errors (#10967)
1 parent 13604de commit f9afef4

4 files changed

Lines changed: 87 additions & 7 deletions

File tree

src/query/service/src/servers/http/v1/mod.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ mod verify;
2727
#[cfg(not(feature = "storage-stage"))]
2828
use databend_common_base::base::ProgressValues;
2929
pub use discovery::discovery_nodes;
30-
#[cfg(not(feature = "storage-stage"))]
3130
use http::StatusCode;
3231
pub use http_query_handlers::QueryResponse;
3332
pub use http_query_handlers::QueryResponseField;
@@ -76,6 +75,24 @@ pub use verify::verify_handler;
7675

7776
pub use crate::servers::http::error::QueryError;
7877

78+
fn require_upload_filename(
79+
field_name: Option<&str>,
80+
filename: Option<&str>,
81+
) -> poem::Result<String> {
82+
match filename.filter(|name| !name.is_empty()) {
83+
Some(name) => Ok(name.to_string()),
84+
None => {
85+
let field_name = field_name.unwrap_or("upload");
86+
Err(poem::Error::from_string(
87+
format!(
88+
"Invalid form-data field '{field_name}': expected a file upload with a filename. If you are using curl, did you forget the '@' in -F '{field_name}=@/path/to/file'?"
89+
),
90+
StatusCode::BAD_REQUEST,
91+
))
92+
}
93+
}
94+
}
95+
7996
#[cfg(not(feature = "storage-stage"))]
8097
#[derive(Serialize, Deserialize, Debug)]
8198
pub struct LoadResponse {

src/query/service/src/servers/http/v1/stage.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use serde::Deserialize;
2929
use serde::Serialize;
3030

3131
use super::HttpQueryContext;
32+
use super::require_upload_filename;
3233
use crate::sessions::TableContext;
3334

3435
#[derive(Serialize, Deserialize, Debug)]
@@ -115,10 +116,7 @@ pub async fn upload_to_stage(
115116

116117
let mut files = vec![];
117118
while let Ok(Some(field)) = multipart.next_field().await {
118-
let name = match field.file_name() {
119-
Some(name) => name.to_string(),
120-
None => uuid::Uuid::new_v4().to_string(),
121-
};
119+
let name = require_upload_filename(field.name(), field.file_name())?;
122120
let file_path = format!("{}/{}", args.relative_path, name)
123121
.trim_start_matches('/')
124122
.to_string();
@@ -141,7 +139,7 @@ pub async fn upload_to_stage(
141139
files.push(name.clone());
142140
}
143141

144-
let mut id = uuid::Uuid::new_v4().to_string();
142+
let id = uuid::Uuid::new_v4().to_string();
145143
Ok(Json(UploadToStageResponse {
146144
id,
147145
stage_name: args.stage_name,

src/query/service/src/servers/http/v1/streaming_load.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ use tokio::sync::mpsc::Sender;
5656
use super::HttpQueryContext;
5757
use super::HttpSessionConf;
5858
use super::HttpSessionStateInternal;
59+
use super::require_upload_filename;
5960
use crate::interpreters::InterpreterFactory;
6061
use crate::servers::http::error::HttpErrorCode;
6162
use crate::servers::http::error::JsonErrorOnly;
@@ -333,7 +334,7 @@ async fn read_multi_part(
333334
StatusCode::BAD_REQUEST,
334335
));
335336
}
336-
let filename = field.file_name().unwrap_or("file_with_no_name").to_string();
337+
let filename = require_upload_filename(name, field.file_name())?;
337338
debug!("Started reading file: {}", &filename);
338339
let mut reader = field.into_async_read();
339340
match file_format {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import requests
2+
3+
4+
query_url = "http://localhost:8000/v1/query"
5+
streaming_url = "http://localhost:8000/v1/streaming_load"
6+
upload_url = "http://localhost:8000/v1/upload_to_stage"
7+
auth = ("root", "")
8+
9+
10+
def execute_sql(sql):
11+
payload = {"sql": sql, "pagination": {"wait_time_secs": 6}}
12+
response = requests.post(
13+
query_url, auth=auth, headers={"Content-Type": "application/json"}, json=payload
14+
)
15+
return response.json()
16+
17+
18+
def missing_at_upload():
19+
return {"upload": (None, "./abc.json")}
20+
21+
22+
def test_upload_to_stage_requires_file_upload():
23+
stage_name = "missing_at_stage"
24+
assert execute_sql(f"drop stage if exists {stage_name}")["error"] == None
25+
assert execute_sql(f"create stage {stage_name}")["error"] == None
26+
27+
try:
28+
response = requests.put(
29+
upload_url,
30+
auth=auth,
31+
headers={"x-databend-stage-name": stage_name},
32+
files=missing_at_upload(),
33+
)
34+
35+
assert response.status_code == 400
36+
assert "expected a file upload with a filename" in response.text
37+
assert "did you forget the '@'" in response.text.lower()
38+
assert "upload=@/path/to/file" in response.text
39+
finally:
40+
assert execute_sql(f"drop stage if exists {stage_name}")["error"] == None
41+
42+
43+
def test_streaming_load_requires_file_upload():
44+
table_name = "missing_at_streaming"
45+
assert execute_sql(f"drop table if exists {table_name}")["error"] == None
46+
assert execute_sql(f"create table {table_name} (a string)")["error"] == None
47+
48+
try:
49+
response = requests.put(
50+
streaming_url,
51+
auth=auth,
52+
headers={
53+
"X-Databend-SQL": f"insert into {table_name} from @_databend_load file_format = (type = csv)",
54+
"x-databend-query-id": "missing-at-streaming-load",
55+
},
56+
files=missing_at_upload(),
57+
)
58+
59+
assert response.status_code == 400
60+
assert "expected a file upload with a filename" in response.text
61+
assert "did you forget the '@'" in response.text.lower()
62+
assert "upload=@/path/to/file" in response.text
63+
finally:
64+
assert execute_sql(f"drop table if exists {table_name}")["error"] == None

0 commit comments

Comments
 (0)