Skip to content

Conversation

ttypic
Copy link
Collaborator

@ttypic ttypic commented Apr 9, 2025

Initial local proxy implementation

Core functionality includes:

  • Spying on HTTP Requests and Responses

    • Inspect outgoing HTTP requests from the client.
    • Inspect and modify incoming HTTP responses.
  • Spying on WebSocket Messages

    • Observe outgoing WebSocket messages from the client.
    • Inspect and modify incoming WebSocket messages.
  • Injecting WebSocket Messages

    • Simulate server messages by injecting WebSocket frames into the client session.
  • Breaking WebSocket and HTTP Connections

    • Force WebSocket disconnections to test reconnection behavior.
    • Simulate slow or broken HTTP responses to test timeout handling.

@ttypic ttypic requested a review from VeskeR April 9, 2025 12:26
@ttypic ttypic force-pushed the ECO-5260/add-intial-proxy-impl branch 2 times, most recently from b21bea7 to 3323ede Compare April 10, 2025 12:25
@ttypic ttypic force-pushed the ECO-5260/add-intial-proxy-impl branch 4 times, most recently from 3a2342b to 3c6051e Compare April 11, 2025 09:48
Core functionality includes:

- Spying on HTTP Requests and Responses
  - Inspect outgoing HTTP requests from the client.
  - Inspect and modify incoming HTTP responses.

- Spying on WebSocket Messages
  - Observe outgoing WebSocket messages from the client.
  - Inspect and modify incoming WebSocket messages.

- Injecting WebSocket Messages
  - Simulate server messages by injecting WebSocket frames into the client session.

- Breaking WebSocket and HTTP Connections
  - Force WebSocket disconnections to test reconnection behavior.
  - Simulate slow or broken HTTP responses to test timeout handling.
@ttypic ttypic force-pushed the ECO-5260/add-intial-proxy-impl branch from 3c6051e to dd96ae9 Compare April 14, 2025 09:24
Copy link

@VeskeR VeskeR left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks interesting!
Observing and injecting APIs by itself should enable replacing a significant amount of private API usage in LiveObjects tests, as these tests often either waited for a specific message to arrive over WebSocket or injected a bunch of messages to emulate server behavior.

I've added some comments in the code below, along with more general suggestions here:

  • Either use the explicit public modifier for public fields everywhere, or omit it everywhere. Currently, it's mixed - see public async start() and pauseAllConnections(): () => void.
  • Regarding naming for proxy public methods: I imagine that methods using the observe* naming convention would accept a callback to continuously notify the caller about events (i.e., actively observing something). However, this is not the case with the current public API, where observe* methods just return the next encountered request or message. Should we rename them to something more better suited, such as waitForNextRequest, waitForNextOutgoingProtocolMessage, or even dropping the waitFor prefix: e.g., proxy.nextRequest() or proxy.nextOutgoingProtocolMessage()?

await observerPromise;
}

export async function openConnection(ws: WebSocket) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably worth renaming to waitForConnectionOpen to keep parity with other methods below


class DefaultCompletableDeferred<T = void> {
private _completeWith!: CompletionHandler<T>;
private value: T | undefined
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it look like value is never set? is it still needed?

@@ -0,0 +1,22 @@
type CompletionHandler<T = void> = (value: T) => void

class DefaultCompletableDeferred<T = void> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would like to have a description for this primitive as it's not immediately obvious what it does.
was it by any chance copied from somewhere? maybe can include a link to source too

try {
await task();
} finally {
deferredValue.complete();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're not catching an error here and I believe it will lead to undandled promise rejections at the line below where we call this.processNext() (as that will throw an error when executin task).
is it intentional here?
also not handling an error prevents us from passing the error to the deferredValue - caller of the async-queue can't know if it failed

const task = this.queue.shift();

try {
await task!!();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await task!!();
await task!();

should it be just one exclamation mark?

proxy.dropConnection(client.connection.id);

// Pause and resume connection manually
const resume = proxy.pauseConnection();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pauseConnection is not in the InterceptingProxy interface. should it be pauseAllConnections?
that being said we should still have a way to pause connection by an id. wdyt?

async () => testWs.send(JSON.stringify({ action: 1 })),
async () => {
const message = await proxy.observeNextIncomingProtocolMessage();
const rawWebsocketMessage = await waitForNextReceivedMessage(testWs);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can these two message be different somehow? why do we need to check both?

private readonly proxy = httpProxy.createProxyServer({});
private readonly wss = new WebSocketServer({ noServer: true });

private readonly server: http.Server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth extracting this createServer logic and similar https://github.com/ably-labs/local-proxy-js/pull/2/files#diff-78bb005ca752086400a648d8aa3e60ce6f666e213da508caabd464d3bfeaf6a2R235 and https://github.com/ably-labs/local-proxy-js/pull/2/files#diff-78bb005ca752086400a648d8aa3e60ce6f666e213da508caabd464d3bfeaf6a2R253 for websocket server to separate functions/classes with a bit more comments to explain what's going on in them.

As far I as I can see these are the parts that intercept http and ws traffic , so we can call them InterceptingHttpServer and InterceptingWebSocketServer

const deferredValue = CompletableDeferred<Request<T>>();
const unregister = this.registerRestMiddleware(async (req, res) => {
if (!filter || filter(req)) deferredValue.complete(req);
return res;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return res;
return req;

shouldn't it return request?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants