Skip to content
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
18 changes: 18 additions & 0 deletions integration-tests/js-compute/fixtures/module-mode/src/kv-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,24 @@ async function kvStoreInterfaceTests() {
);
}

// Test that HttpBody::read_all() correctly handles large list responses
// (i.e. that it uses a growable buffer and doesn't overflow).
// We populate the store with enough 1024-char keys that the list response JSON
// would have exceeded the old 500000-byte fixed buffer.
routes.set('/kv-store/list/large-response', async () => {
const store = new KVStore(KV_STORE_NAME);
// 490 keys × ~1024-char names ≈ 507 KB of JSON
const prefix = 'large-list-' + 'a'.repeat(1009);
await Promise.all(
Array.from({ length: 490 }, (_, i) =>
store.put(prefix + String(i).padStart(4, '0'), 'x'),
),
);
const result = await store.list({});
strictEqual(typeof result, 'object', 'list result is an object');
return new Response('ok');
});

function iteratableToStream(iterable) {
return new ReadableStream({
async pull(controller) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@
"GET /kv-store-options/gen-invalid": { "flake": true },
"GET /kv-store-entry/body": { "flake": true },
"GET /kv-store-entry/bodyUsed": { "flake": true },
"GET /kv-store/list/large-response": { "flake": true },
"GET /transform-stream/identity": {
"downstream_response": { "body": "hello" }
},
Expand Down
30 changes: 22 additions & 8 deletions runtime/fastly/host-api/host_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -998,13 +998,27 @@ Result<size_t> HttpBody::read_into(uint8_t *ptr, size_t chunk_size) const {
}

constexpr size_t HANDLE_READ_CHUNK_SIZE = 8192;
constexpr size_t HANDLE_READ_BUFFER_SIZE = 500000;

Result<HostBytes> HttpBody::read_all() const {
Result<HostBytes> res;
size_t buf_cap = HANDLE_READ_CHUNK_SIZE;
size_t buf_len = 0;
uint8_t *buf = static_cast<uint8_t *>(malloc(HANDLE_READ_BUFFER_SIZE));
uint8_t *buf = static_cast<uint8_t *>(malloc(buf_cap));
if (!buf) {
res.emplace_err(FASTLY_HOST_ERROR_GENERIC_ERROR);
return res;
}
do {
if (buf_len + HANDLE_READ_CHUNK_SIZE > buf_cap) {
buf_cap *= 2;
uint8_t *new_buf = static_cast<uint8_t *>(realloc(buf, buf_cap));
if (!new_buf) {
free(buf);
res.emplace_err(FASTLY_HOST_ERROR_GENERIC_ERROR);
return res;
}
buf = new_buf;
}
host_api::Result<size_t> chunk = this->read_into((buf + buf_len), HANDLE_READ_CHUNK_SIZE);
if (auto *err = chunk.to_err()) {
free(buf);
Expand All @@ -1013,15 +1027,15 @@ Result<HostBytes> HttpBody::read_all() const {
}
size_t len = chunk.unwrap();
if (len == 0) {
buf = static_cast<uint8_t *>(realloc(buf, buf_len));
if (buf_len == 0) {
free(buf);
buf = nullptr;
} else {
buf = static_cast<uint8_t *>(realloc(buf, buf_len));
}
break;
}
buf_len += len;
if (buf_len > HANDLE_READ_BUFFER_SIZE) {
free(buf);
res.emplace_err(FASTLY_HOST_ERROR_BUFFER_LEN);
return res;
}
} while (true);
res.emplace(make_host_bytes(buf, buf_len));
return res;
Expand Down
Loading