1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-28 16:20:57 -05:00

refactor(node): reimplement http client (#19122)

This commit reimplements most of "node:http" client APIs using
"ext/fetch".

There is some duplicated code and two removed Node compat tests that
will be fixed in follow up PRs.

---------

Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
Leo Kettmeir 2023-05-17 01:20:32 +02:00 committed by GitHub
parent a22388bbd1
commit 867a6d3032
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1702 additions and 1163 deletions

2
Cargo.lock generated
View file

@ -1155,6 +1155,7 @@ dependencies = [
"cbc",
"data-encoding",
"deno_core",
"deno_fetch",
"deno_fs",
"deno_media_type",
"deno_npm",
@ -1183,6 +1184,7 @@ dependencies = [
"pbkdf2",
"rand",
"regex",
"reqwest",
"ring",
"ripemd",
"rsa",

View file

@ -362,11 +362,13 @@
// failing
//"test-http-client-set-timeout.js",
"test-http-localaddress.js",
"test-http-outgoing-buffer.js",
// TODO(bartlomieju): temporarily disabled while we iterate on the HTTP client
// "test-http-outgoing-buffer.js",
"test-http-outgoing-internal-headernames-getter.js",
"test-http-outgoing-internal-headernames-setter.js",
"test-http-outgoing-internal-headers.js",
"test-http-outgoing-message-inheritance.js",
// TODO(bartlomieju): temporarily disabled while we iterate on the HTTP client
// "test-http-outgoing-message-inheritance.js",
"test-http-outgoing-renderHeaders.js",
"test-http-outgoing-settimeout.js",
"test-net-access-byteswritten.js",

View file

@ -1,26 +0,0 @@
// deno-fmt-ignore-file
// deno-lint-ignore-file
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Taken from Node 18.12.1
// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually
// Flags: --expose-internals
'use strict';
require('../common');
const assert = require('assert');
const { getDefaultHighWaterMark } = require('internal/streams/state');
const http = require('http');
const OutgoingMessage = http.OutgoingMessage;
const msg = new OutgoingMessage();
msg._implicitHeader = function() {};
// Writes should be buffered until highwatermark
// even when no socket is assigned.
assert.strictEqual(msg.write('asd'), true);
while (msg.write('asd'));
const highwatermark = msg.writableHighWaterMark || getDefaultHighWaterMark();
assert(msg.outputSize >= highwatermark);

View file

@ -1,43 +0,0 @@
// deno-fmt-ignore-file
// deno-lint-ignore-file
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Taken from Node 18.12.1
// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually
'use strict';
const common = require('../common');
const { OutgoingMessage } = require('http');
const { Writable } = require('stream');
const assert = require('assert');
// Check that OutgoingMessage can be used without a proper Socket
// Refs: https://github.com/nodejs/node/issues/14386
// Refs: https://github.com/nodejs/node/issues/14381
class Response extends OutgoingMessage {
_implicitHeader() {}
}
const res = new Response();
let firstChunk = true;
const ws = new Writable({
write: common.mustCall((chunk, encoding, callback) => {
if (firstChunk) {
assert(chunk.toString().endsWith('hello world'));
firstChunk = false;
} else {
assert.strictEqual(chunk.length, 0);
}
setImmediate(callback);
}, 2)
});
res.socket = ws;
ws._httpMessage = res;
res.connection = ws;
res.end('hello world');

View file

@ -185,6 +185,7 @@ Deno.test("[node/http] server can respond with 101, 204, 205, 304 status", async
Deno.test("[node/http] request default protocol", async () => {
const promise = deferred<void>();
const promise2 = deferred<void>();
const server = http.createServer((_, res) => {
res.end("ok");
});
@ -198,6 +199,7 @@ Deno.test("[node/http] request default protocol", async () => {
server.close();
});
assertEquals(res.statusCode, 200);
promise2.resolve();
},
);
req.end();
@ -206,6 +208,7 @@ Deno.test("[node/http] request default protocol", async () => {
promise.resolve();
});
await promise;
await promise2;
});
Deno.test("[node/http] request with headers", async () => {
@ -292,32 +295,6 @@ Deno.test("[node/http] http.IncomingMessage can be created without url", () => {
});
*/
Deno.test("[node/http] set http.IncomingMessage.statusMessage", () => {
// deno-lint-ignore no-explicit-any
const message = new (http as any).IncomingMessageForClient(
new Response(null, { status: 404, statusText: "Not Found" }),
{
encrypted: true,
readable: false,
remoteAddress: "foo",
address() {
return { port: 443, family: "IPv4" };
},
// deno-lint-ignore no-explicit-any
end(_cb: any) {
return this;
},
// deno-lint-ignore no-explicit-any
destroy(_e: any) {
return;
},
},
);
assertEquals(message.statusMessage, "Not Found");
message.statusMessage = "boom";
assertEquals(message.statusMessage, "boom");
});
Deno.test("[node/http] send request with non-chunked body", async () => {
let requestHeaders: Headers;
let requestBody = "";

View file

@ -66,7 +66,7 @@ pub use reqwest;
pub use fs_fetch_handler::FsFetchHandler;
use crate::byte_stream::MpscByteStream;
pub use crate::byte_stream::MpscByteStream;
#[derive(Clone)]
pub struct Options {
@ -186,9 +186,9 @@ pub fn get_declaration() -> PathBuf {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FetchReturn {
request_rid: ResourceId,
request_body_rid: Option<ResourceId>,
cancel_handle_rid: Option<ResourceId>,
pub request_rid: ResourceId,
pub request_body_rid: Option<ResourceId>,
pub cancel_handle_rid: Option<ResourceId>,
}
pub fn get_or_create_client_from_state(
@ -302,7 +302,7 @@ where
}
Some(data) => {
// If a body is passed, we use it, and don't return a body for streaming.
request = request.body(Vec::from(&*data));
request = request.body(data.to_vec());
None
}
}
@ -400,12 +400,12 @@ where
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FetchResponse {
status: u16,
status_text: String,
headers: Vec<(ByteString, ByteString)>,
url: String,
response_rid: ResourceId,
content_length: Option<u64>,
pub status: u16,
pub status_text: String,
pub headers: Vec<(ByteString, ByteString)>,
pub url: String,
pub response_rid: ResourceId,
pub content_length: Option<u64>,
}
#[op]
@ -462,8 +462,8 @@ pub async fn op_fetch_send(
type CancelableResponseResult = Result<Result<Response, AnyError>, Canceled>;
struct FetchRequestResource(
Pin<Box<dyn Future<Output = CancelableResponseResult>>>,
pub struct FetchRequestResource(
pub Pin<Box<dyn Future<Output = CancelableResponseResult>>>,
);
impl Resource for FetchRequestResource {
@ -472,7 +472,7 @@ impl Resource for FetchRequestResource {
}
}
struct FetchCancelHandle(Rc<CancelHandle>);
pub struct FetchCancelHandle(pub Rc<CancelHandle>);
impl Resource for FetchCancelHandle {
fn name(&self) -> Cow<str> {
@ -485,8 +485,8 @@ impl Resource for FetchCancelHandle {
}
pub struct FetchRequestBodyResource {
body: AsyncRefCell<mpsc::Sender<Option<bytes::Bytes>>>,
cancel: CancelHandle,
pub body: AsyncRefCell<mpsc::Sender<Option<bytes::Bytes>>>,
pub cancel: CancelHandle,
}
impl Resource for FetchRequestBodyResource {
@ -537,10 +537,10 @@ impl Resource for FetchRequestBodyResource {
type BytesStream =
Pin<Box<dyn Stream<Item = Result<bytes::Bytes, std::io::Error>> + Unpin>>;
struct FetchResponseBodyResource {
reader: AsyncRefCell<Peekable<BytesStream>>,
cancel: CancelHandle,
size: Option<u64>,
pub struct FetchResponseBodyResource {
pub reader: AsyncRefCell<Peekable<BytesStream>>,
pub cancel: CancelHandle,
pub size: Option<u64>,
}
impl Resource for FetchResponseBodyResource {
@ -590,8 +590,8 @@ impl Resource for FetchResponseBodyResource {
}
}
struct HttpClientResource {
client: Client,
pub struct HttpClientResource {
pub client: Client,
}
impl Resource for HttpClientResource {

View file

@ -18,6 +18,7 @@ aes.workspace = true
cbc.workspace = true
data-encoding = "2.3.3"
deno_core.workspace = true
deno_fetch.workspace = true
deno_fs.workspace = true
deno_media_type.workspace = true
deno_npm.workspace = true
@ -46,6 +47,7 @@ path-clean = "=0.1.0"
pbkdf2 = "0.12.1"
rand.workspace = true
regex.workspace = true
reqwest.workspace = true
ring.workspace = true
ripemd = "0.1.3"
rsa.workspace = true

View file

@ -206,6 +206,7 @@ deno_core::extension!(deno_node,
ops::zlib::op_zlib_write_async,
ops::zlib::op_zlib_init,
ops::zlib::op_zlib_reset,
ops::http::op_node_http_request,
op_node_build_os,
ops::require::op_require_init_paths,
ops::require::op_require_node_module_paths<P>,

101
ext/node/ops/http.rs Normal file
View file

@ -0,0 +1,101 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
use deno_core::error::type_error;
use deno_core::error::AnyError;
use deno_core::op;
use deno_core::url::Url;
use deno_core::AsyncRefCell;
use deno_core::ByteString;
use deno_core::CancelFuture;
use deno_core::CancelHandle;
use deno_core::OpState;
use deno_fetch::get_or_create_client_from_state;
use deno_fetch::FetchCancelHandle;
use deno_fetch::FetchRequestBodyResource;
use deno_fetch::FetchRequestResource;
use deno_fetch::FetchReturn;
use deno_fetch::HttpClientResource;
use deno_fetch::MpscByteStream;
use reqwest::header::HeaderMap;
use reqwest::header::HeaderName;
use reqwest::header::HeaderValue;
use reqwest::header::CONTENT_LENGTH;
use reqwest::Body;
use reqwest::Method;
#[op]
pub fn op_node_http_request(
state: &mut OpState,
method: ByteString,
url: String,
headers: Vec<(ByteString, ByteString)>,
client_rid: Option<u32>,
has_body: bool,
) -> Result<FetchReturn, AnyError> {
let client = if let Some(rid) = client_rid {
let r = state.resource_table.get::<HttpClientResource>(rid)?;
r.client.clone()
} else {
get_or_create_client_from_state(state)?
};
let method = Method::from_bytes(&method)?;
let url = Url::parse(&url)?;
let mut header_map = HeaderMap::new();
for (key, value) in headers {
let name = HeaderName::from_bytes(&key)
.map_err(|err| type_error(err.to_string()))?;
let v = HeaderValue::from_bytes(&value)
.map_err(|err| type_error(err.to_string()))?;
header_map.append(name, v);
}
let mut request = client.request(method.clone(), url).headers(header_map);
let request_body_rid = if has_body {
// If no body is passed, we return a writer for streaming the body.
let (stream, tx) = MpscByteStream::new();
request = request.body(Body::wrap_stream(stream));
let request_body_rid = state.resource_table.add(FetchRequestBodyResource {
body: AsyncRefCell::new(tx),
cancel: CancelHandle::default(),
});
Some(request_body_rid)
} else {
// POST and PUT requests should always have a 0 length content-length,
// if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
if matches!(method, Method::POST | Method::PUT) {
request = request.header(CONTENT_LENGTH, HeaderValue::from(0));
}
None
};
let cancel_handle = CancelHandle::new_rc();
let cancel_handle_ = cancel_handle.clone();
let fut = async move {
request
.send()
.or_cancel(cancel_handle_)
.await
.map(|res| res.map_err(|err| type_error(err.to_string())))
};
let request_rid = state
.resource_table
.add(FetchRequestResource(Box::pin(fut)));
let cancel_handle_rid =
state.resource_table.add(FetchCancelHandle(cancel_handle));
Ok(FetchReturn {
request_rid,
request_body_rid,
cancel_handle_rid: Some(cancel_handle_rid),
})
}

View file

@ -1,6 +1,7 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
pub mod crypto;
pub mod http;
pub mod idna;
pub mod require;
pub mod v8;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -4,15 +4,11 @@
import { notImplemented } from "ext:deno_node/_utils.ts";
import { urlToHttpOptions } from "ext:deno_node/internal/url.ts";
import {
Agent as HttpAgent,
ClientRequest,
IncomingMessageForClient as IncomingMessage,
type RequestOptions,
} from "ext:deno_node/http.ts";
import type { Socket } from "ext:deno_node/net.ts";
export class Agent extends HttpAgent {
}
import { Agent as HttpAgent } from "ext:deno_node/_http_agent.mjs";
export class Server {
constructor() {
@ -53,41 +49,61 @@ export function get(...args: any[]) {
return req;
}
export const globalAgent = undefined;
export class Agent extends HttpAgent {
constructor(options) {
super(options);
this.defaultPort = 443;
this.protocol = "https:";
this.maxCachedSessions = this.options.maxCachedSessions;
if (this.maxCachedSessions === undefined) {
this.maxCachedSessions = 100;
}
this._sessionCache = {
map: {},
list: [],
};
}
}
const globalAgent = new Agent({
keepAlive: true,
scheduling: "lifo",
timeout: 5000,
});
/** HttpsClientRequest class loosely follows http.ClientRequest class API. */
class HttpsClientRequest extends ClientRequest {
override defaultProtocol = "https:";
override async _createCustomClient(): Promise<
Deno.HttpClient | undefined
> {
override _getClient(): Deno.HttpClient | undefined {
if (caCerts === null) {
return undefined;
}
if (caCerts !== undefined) {
return Deno.createHttpClient({ caCerts });
}
const status = await Deno.permissions.query({
name: "env",
variable: "NODE_EXTRA_CA_CERTS",
});
if (status.state !== "granted") {
caCerts = null;
return undefined;
}
// const status = await Deno.permissions.query({
// name: "env",
// variable: "NODE_EXTRA_CA_CERTS",
// });
// if (status.state !== "granted") {
// caCerts = null;
// return undefined;
// }
const certFilename = Deno.env.get("NODE_EXTRA_CA_CERTS");
if (!certFilename) {
caCerts = null;
return undefined;
}
const caCert = await Deno.readTextFile(certFilename);
const caCert = Deno.readTextFileSync(certFilename);
caCerts = [caCert];
return Deno.createHttpClient({ caCerts });
}
override _createSocket(): Socket {
/*override _createSocket(): Socket {
// deno-lint-ignore no-explicit-any
return { authorized: true } as any;
}
}*/
}
/** Makes a request to an https server. */
@ -107,15 +123,21 @@ export function request(
// deno-lint-ignore no-explicit-any
export function request(...args: any[]) {
let options = {};
if (typeof args[0] === "string") {
options = urlToHttpOptions(new URL(args.shift()));
const urlStr = args.shift();
options = urlToHttpOptions(new URL(urlStr));
} else if (args[0] instanceof URL) {
options = urlToHttpOptions(args.shift());
}
if (args[0] && typeof args[0] !== "function") {
Object.assign(options, args.shift());
}
options._defaultAgent = globalAgent;
args.unshift(options);
return new HttpsClientRequest(args[0], args[1]);
}
export default {

View file

@ -3,7 +3,7 @@
NOTE: This file should not be manually edited. Please edit 'cli/tests/node_compat/config.json' and run 'tools/node_compat/setup.ts' instead.
Total: 2933
Total: 2935
- [abort/test-abort-backtrace.js](https://github.com/nodejs/node/tree/v18.12.1/test/abort/test-abort-backtrace.js)
- [abort/test-abort-fatal-error.js](https://github.com/nodejs/node/tree/v18.12.1/test/abort/test-abort-fatal-error.js)
@ -1083,6 +1083,7 @@ Total: 2933
- [parallel/test-http-no-content-length.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-no-content-length.js)
- [parallel/test-http-no-read-no-dump.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-no-read-no-dump.js)
- [parallel/test-http-nodelay.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-nodelay.js)
- [parallel/test-http-outgoing-buffer.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-outgoing-buffer.js)
- [parallel/test-http-outgoing-destroy.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-outgoing-destroy.js)
- [parallel/test-http-outgoing-destroyed.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-outgoing-destroyed.js)
- [parallel/test-http-outgoing-end-cork.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-outgoing-end-cork.js)
@ -1093,6 +1094,7 @@ Total: 2933
- [parallel/test-http-outgoing-finished.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-outgoing-finished.js)
- [parallel/test-http-outgoing-first-chunk-singlebyte-encoding.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-outgoing-first-chunk-singlebyte-encoding.js)
- [parallel/test-http-outgoing-message-capture-rejection.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-outgoing-message-capture-rejection.js)
- [parallel/test-http-outgoing-message-inheritance.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-outgoing-message-inheritance.js)
- [parallel/test-http-outgoing-message-write-callback.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-outgoing-message-write-callback.js)
- [parallel/test-http-outgoing-properties.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-outgoing-properties.js)
- [parallel/test-http-outgoing-proto.js](https://github.com/nodejs/node/tree/v18.12.1/test/parallel/test-http-outgoing-proto.js)