Skip to content

Feature: Add support for application/x-www-form-urlencoded encoding #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ LOAD http_client;
### Functions
- `http_get(url)`
- `http_post(url, headers, params)`
- Sends POST request with params encoded as a JSON object
- `http_post_form(url, headers, params)`
- Sends POST request with params being `application/x-www-form-urlencoded` encoded (used by many forms and some APIs)

### Examples
#### GET
Expand Down Expand Up @@ -83,6 +86,44 @@ D WITH __input AS (
└────────┴─────────┴─────────────┘
```

#### POST using form encoding(application/x-www-form-urlencoded, not multipart/form-data)
```sql
D WITH __input AS (
SELECT
http_post_form(
'https://httpbin.org/delay/0',
headers => MAP {
'accept': 'application/json',
},
params => MAP {
'limit': 10
}
) AS res
),
__response AS (
SELECT
(res->>'status')::INT AS status,
(res->>'reason') AS reason,
unnest( from_json(((res->>'body')::JSON)->'form', '{"limit": "VARCHAR"}') ) AS features
FROM
__input
)
SELECT
__response.status,
__response.reason,
__response.limit AS limit
FROM
__response
;
┌────────┬─────────┬─────────┐
│ status │ reason │ limit │
│ int32 │ varchar │ varchar │
├────────┼─────────┼─────────┤
│ 200 │ OK │ 10 │
└────────┴─────────┴─────────┘
```


#### Full Example w/ spatial data
This is the original example by @ahuarte47 inspiring this community extension.

Expand Down
47 changes: 47 additions & 0 deletions src/http_client_extension.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,46 @@ static void HTTPPostRequestFunction(DataChunk &args, ExpressionState &state, Vec
});
}

static void HTTPPostFormRequestFunction(DataChunk &args, ExpressionState &state, Vector &result) {
D_ASSERT(args.data.size() == 3);

using STRING_TYPE = PrimitiveType<string_t>;
using LENTRY_TYPE = PrimitiveType<list_entry_t>;

auto &url_vector = args.data[0];
auto &headers_vector = args.data[1];
auto &headers_entry = ListVector::GetEntry(headers_vector);
auto &body_vector = args.data[2];
auto &body_entry = ListVector::GetEntry(body_vector);

GenericExecutor::ExecuteTernary<STRING_TYPE, LENTRY_TYPE, LENTRY_TYPE, STRING_TYPE>(
url_vector, headers_vector, body_vector, result, args.size(),
[&](STRING_TYPE url, LENTRY_TYPE headers, LENTRY_TYPE params) {
std::string url_str = url.val.GetString();

// Use helper to setup client and parse URL
auto client_and_path = SetupHttpClient(url_str);
auto &client = client_and_path.first;
auto &path = client_and_path.second;

// Prepare headers and parameters
duckdb_httplib_openssl::Headers header_map;
duckdb_httplib_openssl::Params params_map;
ConvertListEntryToMap<duckdb_httplib_openssl::Headers>(headers.val, headers_entry, header_map);
ConvertListEntryToMap<duckdb_httplib_openssl::Params>(params.val, body_entry, params_map);

// Make the POST request with headers and params
auto res = client.Post(path.c_str(), header_map, params_map);
if (res) {
std::string response = GetJsonResponse(res->status, res->reason, res->body);
return StringVector::AddString(result, response);
} else {
std::string response = GetJsonResponse(-1, GetHttpErrorMessage(res, "POST"), "");
return StringVector::AddString(result, response);
}
});
}


static void LoadInternal(DatabaseInstance &instance) {
ScalarFunctionSet http_get("http_get");
Expand All @@ -276,6 +316,13 @@ static void LoadInternal(DatabaseInstance &instance) {
{LogicalType::VARCHAR, LogicalType::MAP(LogicalType::VARCHAR, LogicalType::VARCHAR), LogicalType::JSON()},
LogicalType::JSON(), HTTPPostRequestFunction));
ExtensionUtil::RegisterFunction(instance, http_post);

ScalarFunctionSet http_post_form("http_post_form");
http_post_form.AddFunction(ScalarFunction(
{LogicalType::VARCHAR, LogicalType::MAP(LogicalType::VARCHAR, LogicalType::VARCHAR),
LogicalType::MAP(LogicalType::VARCHAR, LogicalType::VARCHAR)},
LogicalType::JSON(), HTTPPostFormRequestFunction));
ExtensionUtil::RegisterFunction(instance, http_post_form);
}

void HttpClientExtension::Load(DuckDB &db) {
Expand Down
65 changes: 65 additions & 0 deletions test/sql/httpclient.test
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,68 @@ FROM
;
----
S2A_56LPN_20210930_0_L2A

# Confirm the POST function with form request works
query III
WITH __input AS (
SELECT
http_post_form(
'https://httpbin.org/delay/0',
headers => MAP {
'accept': 'application/json',
},
params => MAP {
'limit': 10
}
) AS res
),
__response AS (
SELECT
(res->>'status')::INT AS status,
(res->>'reason') AS reason,
unnest( from_json(((res->>'body')::JSON)->'headers', '{"Host": "VARCHAR"}') ) AS features
FROM
__input
)
SELECT
__response.status,
__response.reason,
__response.Host AS host
FROM
__response
;
----
200 OK httpbin.org

# Confirm the POST function with form encoding transmits a single value
query III
WITH __input AS (
SELECT
http_post_form(
'https://httpbin.org/delay/0',
headers => MAP {
'accept': 'application/json',
},
params => MAP {
'limit': 10
}
) AS res
),
__response AS (
SELECT
(res->>'status')::INT AS status,
(res->>'reason') AS reason,
unnest( from_json(((res->>'body')::JSON)->'form', '{"limit": "VARCHAR"}') ) AS features
FROM
__input
)
SELECT
__response.status,
__response.reason,
__response.limit AS limit
FROM
__response
;
----
200 OK 10

Loading