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
46 changes: 24 additions & 22 deletions impit-node/index.wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ Run your script with IMPIT_VERBOSE=1 environment variable to get more informatio

class ResponsePatches {
static async text() {
const buffer = await this.bytes();
return this.decodeBuffer(buffer);
const buffer = await this.arrayBuffer();
return this.decodeBuffer(Buffer.from(buffer));
}
}

Expand All @@ -39,14 +39,18 @@ function canonicalizeHeaders(headers) {
return [];
}

const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);

function isRedirectStatus(status) {
return [301, 302, 303, 307, 308].includes(status);
return REDIRECT_STATUSES.has(status);
}

const textEncoder = new TextEncoder();

// Stream chunks may be strings (e.g. a ReadableStream or Node stream that wasn't
// fed bytes); encode them so they aren't coerced to zero bytes by Uint8Array.set.
function toUint8Array(chunk) {
return typeof chunk === 'string' ? new TextEncoder().encode(chunk) : new Uint8Array(chunk);
return typeof chunk === 'string' ? textEncoder.encode(chunk) : new Uint8Array(chunk);
}

// Streamed bodies are passed to the native layer as a ReadableStream of bytes, so that the chunks
Expand All @@ -69,7 +73,7 @@ function toByteStream(source) {

function shouldRewriteRedirectToGet(httpStatus, method) {
// See https://github.com/mozilla-firefox/firefox/blob/911b3eec6c5e58a9a49e23aa105e49aa76e00f9c/netwerk/protocol/http/HttpBaseChannel.cpp#L4801
if ([301, 302].includes(httpStatus)) {
if (httpStatus === 301 || httpStatus === 302) {
return method === 'POST';
}

Expand Down Expand Up @@ -138,7 +142,6 @@ class Impit extends native.Impit {

const blobParts = [];
const rn = new Uint8Array([13, 10]);
const textEncoder = new TextEncoder();

for (const [name, value] of formData) {
if (typeof value === 'string') {
Expand Down Expand Up @@ -189,9 +192,9 @@ class Impit extends native.Impit {
// Based on https://github.com/nodejs/undici/blob/14e62db0d0cff4bea27357aa5bd14881459b27c7/lib/web/fetch/body.js#L90
async #serializeBody(body) {
if (typeof body === 'string') {
return { body: new TextEncoder().encode(body), type: 'text/plain;charset=UTF-8' };
return { body: textEncoder.encode(body), type: 'text/plain;charset=UTF-8' };
} else if (body instanceof URLSearchParams) {
return { body: new TextEncoder().encode(body.toString()), type: 'application/x-www-form-urlencoded;charset=UTF-8' };
return { body: textEncoder.encode(body.toString()), type: 'application/x-www-form-urlencoded;charset=UTF-8' };
} else if (body instanceof ArrayBuffer) {
return { body: new Uint8Array(body.slice()), type: '' };
} else if (ArrayBuffer.isView(body)) {
Expand Down Expand Up @@ -261,9 +264,9 @@ class Impit extends native.Impit {
signal?.throwIfAborted();

let abortHandler;
const waitForAbort = new Promise((_, reject) => {
const waitForAbort = signal && new Promise((_, reject) => {
abortHandler = () => reject(signal.reason);
signal?.addEventListener?.("abort", abortHandler, { once: true });
signal.addEventListener?.("abort", abortHandler, { once: true });
});

try {
Expand Down Expand Up @@ -296,13 +299,12 @@ class Impit extends native.Impit {
while (true) {
signal?.throwIfAborted();

const headers = [...(options.headers || [])];
const hasUserCookie = headers.some(([k]) => k.toLowerCase() === 'cookie');
let headers = options.headers || [];

if (this.#cookieJar && !hasUserCookie) {
if (this.#cookieJar && !headers.some(([k]) => k.toLowerCase() === 'cookie')) {
const cookieHeader = await this.#getCookies(url);
if (cookieHeader) {
headers.push(['Cookie', cookieHeader]);
headers = [...headers, ['Cookie', cookieHeader]];
}
}

Expand All @@ -314,10 +316,9 @@ class Impit extends native.Impit {
bodyStream: method === 'GET' ? undefined : options.bodyStream,
});

const originalResponse = await Promise.race([
response,
waitForAbort
]);
const originalResponse = waitForAbort
? await Promise.race([response, waitForAbort])
: await response;

const responseHeaders = new Headers(originalResponse.headers);

Expand All @@ -334,7 +335,7 @@ class Impit extends native.Impit {
const location = responseHeaders.get('location');

if (!location) {
return this.#wrapResponse(originalResponse, signal);
return this.#wrapResponse(originalResponse, signal, responseHeaders);
}

redirectCount++;
Expand All @@ -353,17 +354,18 @@ class Impit extends native.Impit {
}
}

return this.#wrapResponse(originalResponse, signal);
return this.#wrapResponse(originalResponse, signal, responseHeaders);
}
}

/**
* Wrap a native response with JS enhancements
* @param {object} originalResponse
* @param {AbortSignal} signal
* @param {Headers} headers Response headers, already parsed by the caller
* @returns {object}
*/
#wrapResponse(originalResponse, signal) {
#wrapResponse(originalResponse, signal, headers) {
signal?.throwIfAborted();

let abortHandler;
Expand Down Expand Up @@ -411,7 +413,7 @@ class Impit extends native.Impit {
});

Object.defineProperty(originalResponse, 'headers', {
value: new Headers(originalResponse.headers)
value: headers
});

Object.defineProperty(originalResponse, 'clone', {
Expand Down
35 changes: 16 additions & 19 deletions impit-node/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,30 +120,27 @@ impl ImpitWrapper {
url: String,
request_init: Option<RequestInit>,
) -> Result<ImpitResponse, napi::Error> {
let RequestInit {
method,
headers,
body,
body_stream,
timeout,
force_http3,
..
} = request_init.unwrap_or_default();

let request_options = Some(RequestOptions {
headers: request_init
.as_ref()
.and_then(|init| init.headers.as_ref())
.cloned()
.unwrap_or_default(),
timeout: request_init
.as_ref()
.and_then(|init| init.timeout)
.map(|timeout| Some(Duration::from_millis(timeout.into()))),
http3_prior_knowledge: request_init
.as_ref()
.and_then(|init| init.force_http3)
.unwrap_or_default(),
headers: headers.unwrap_or_default(),
timeout: timeout.map(|timeout| Some(Duration::from_millis(timeout.into()))),
http3_prior_knowledge: force_http3.unwrap_or_default(),
});

let method = request_init
.as_ref()
.and_then(|init| init.method.to_owned())
.unwrap_or_default();
let body = request_init.and_then(|init| match (init.body, init.body_stream) {
let method = method.unwrap_or_default();
let body = match (body, body_stream) {
(_, Some(stream)) => Some(ImpitBody::from_stream(stream.into_byte_stream())),
(bytes, None) => bytes.map(|array| array.to_vec().into()),
});
};

let response = if matches!(method, HttpMethod::Get | HttpMethod::Head) && body.is_some() {
Err(ImpitError::BindingPassthroughError(
Expand Down
2 changes: 1 addition & 1 deletion impit-node/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ impl<'env> ImpitResponse {
// JS Fetch semantics: header values are decoded as ISO-8859-1 (each byte 0x00..=0xFF maps to
// the code point U+0000..=U+00FF). Since that mapping is a bijection, the string stays
// byte-recoverable via `Buffer.from(value, 'latin1')`.
let mut headers_vec: Vec<(String, String)> = Vec::new();
let mut headers_vec: Vec<(String, String)> = Vec::with_capacity(response.headers().len());
for (k, v) in response.headers().iter() {
headers_vec.push((
k.as_str().to_string(),
Expand Down
10 changes: 2 additions & 8 deletions impit-python/src/async_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,13 +376,11 @@ impl AsyncClient {
url: String,
content: Option<RequestBody>,
mut data: Option<RequestBody>,
headers: Option<HashMap<String, String>>,
mut headers: Option<HashMap<String, String>>,
timeout: Option<Either<f64, &str>>,
force_http3: Option<bool>,
stream: Option<bool>,
) -> Result<pyo3::Bound<'python, PyAny>, PyErr> {
let mut headers = headers.clone();

if let Some(content) = content {
data = Some(content);
}
Expand All @@ -392,11 +390,7 @@ impl AsyncClient {
let timeout = parse_timeout(timeout)?;

let options = RequestOptions {
headers: headers
.unwrap_or_default()
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
headers: headers.unwrap_or_default().into_iter().collect(),
timeout,
http3_prior_knowledge: force_http3.unwrap_or(false),
};
Expand Down
10 changes: 2 additions & 8 deletions impit-python/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,13 +373,11 @@ impl Client {
url: String,
content: Option<RequestBody>,
mut data: Option<RequestBody>,
headers: Option<HashMap<String, String>>,
mut headers: Option<HashMap<String, String>>,
timeout: Option<Either<f64, &str>>,
force_http3: Option<bool>,
stream: Option<bool>,
) -> Result<ImpitPyResponse, ImpitPyError> {
let mut headers = headers.clone();

if let Some(content) = content {
data = Some(content);
}
Expand All @@ -391,11 +389,7 @@ impl Client {
.map_err(|e| ImpitPyError(ImpitError::BindingPassthroughError(e.to_string())))?;

let options = RequestOptions {
headers: headers
.unwrap_or_default()
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
headers: headers.unwrap_or_default().into_iter().collect(),
timeout,
http3_prior_knowledge: force_http3.unwrap_or(false),
};
Expand Down
Loading