fix: properly handle Unix socket paths in interceptors#722
fix: properly handle Unix socket paths in interceptors#722vinialbano wants to merge 2 commits intomswjs:mainfrom
Conversation
|
Hi @kettanaito, were you able to take a look at this by any chance? |
|
I'm wondering what the expected behavior is from this lib POV. Should we intercept them or automatically pass through? |
That's a great question @mikicho. Could this introduce any vulnerabilities if requests were intercepted? I see no harm in passing them through. I recognize some use cases for intercepting them, but they should probably be rarely explored, and I'm not sure if any security issues might arise. |
|
Sorry, I didn't have the time to look into this. Will do my best to review this this month. |
kettanaito
left a comment
There was a problem hiding this comment.
Thank you so much for working on this!
I think we should handle socketPath properly. That being said, I disagree with some of the directions taken by this solution. Please see my comments.
The parts I do like:
- The normalize client args test;
- The url-from-options test;
- The fixes for the above seem good.
My main concern is that socketPath is a request option, not an agent option, and as such, drilling it through MockAgent is wrong. We should explore other ways to reference that option, like from within MockHttpSocket itself.
|
|
||
| // For Unix socket paths, create a direct socket connection | ||
| if (this.connectionOptions.socketPath) { | ||
| socket = net.createConnection({ path: this.connectionOptions.socketPath }) |
There was a problem hiding this comment.
this.createConnection() should already encapsulate net.createConnection(connectionOptions) within itself. Does it not?
| this.customAgent = options.customAgent | ||
| this.onRequest = options.onRequest | ||
| this.onResponse = options.onResponse | ||
| this.socketPath = options.socketPath |
There was a problem hiding this comment.
But is socketPath an actual property on http.Agent? If we are drilling this here for convenience, and the real agent doesn't in fact list socketPath, we should rethink the approach.
|
|
||
| describe('Unix socket handling', () => { | ||
| // Skip these tests in environments that don't support Unix sockets | ||
| if (process.platform === 'win32') { |
There was a problem hiding this comment.
We don't run tests on Windows so this can be dropped altogether.
| expect(res.headers['x-custom-header']).toEqual('Yes') | ||
| }) | ||
|
|
||
| describe('Unix socket handling', () => { |
There was a problem hiding this comment.
If possible, please adhere to the flat test case structure. Thanks!
There was a problem hiding this comment.
In fact, what you are solving warrants either a compliance or regression test. It would be great to put the new test case in the respective folder.
| } | ||
|
|
||
| // When | ||
| const agent = new agents.MockAgent(agentOptions) |
There was a problem hiding this comment.
This isn't a correct test. I appreciate the unit test approach here but we don't do that with Interceptors because our goal is to provide compliance with existing request clients.
This test has to be rewritten without using our internal classes at all. Just plain http.* usage as the client would use it.
| customAgent: options.agent, | ||
| onRequest, | ||
| onResponse, | ||
| ...(options.socketPath && { socketPath: options.socketPath }), |
There was a problem hiding this comment.
I believe this answers my previous concern about the socketPath being an agent option. It is not. Here, we are manually making it be one and that breaches the responsibility of the MockAgent class.
We should rethink this approach. Note that you should be able to get the original ClientRequest options from within the MockHttpSocket. Perhaps that would be a better solution.
| @@ -1,31 +1,31 @@ | |||
| import { urlToHttpOptions } from 'node:url' | |||
| import { Logger } from '@open-draft/logger' | |||
There was a problem hiding this comment.
Do you have an import sorter plugin or similar? Please disable and revert formatting on this file.
| @@ -1,4 +1,17 @@ | |||
| export function baseUrlFromConnectionOptions(options: any): URL { | |||
| // Handle Unix socket paths | |||
| if (options.socketPath) { | |||
There was a problem hiding this comment.
Because socketPath and request URL properties are mutually exclusive, right? One cannot provide both?
| export type ResolvedRequestOptions = RequestOptions & RequestSelf | ||
| export type ResolvedRequestOptions = RequestOptions & | ||
| RequestSelf & { | ||
| socketPath?: string |
There was a problem hiding this comment.
So socketPath isn't annotated on RequestOptions by Node.js? That's a pity.
|
@mikicho, correct me if I'm wrong, but |
|
@kettanaito I'm not familiar with this either. From what I understand, it is a Unix way to communicate between processes. |
|
@kettanaito @vinialbano I think the root cause is that we create a custom agent and then override the const request = http.get({
socketPath,
path: '/test-get',
agent: false, // Disable the default agent
})@kettanaito I believe this logic is outdated and no longer applicable to the socket-level interceptor we are currently using. After running a quick check, I noticed that all the tests still passed when I commented out these lines. We may want to remove it. |
|
@mikicho, I think you are right regarding the default agent. Nice find! |
|
Do we need any additional changes to support Unix socket paths after #731? |
UpdateI've moved the tests introduced in this pull request to #751, where they are passing without any additional changes. It looks like #731 has indeed fixed this issue. Interestingly enough, we already had Unix socket tests at |
|
Since the tests are passing, I'm consider this issue non-reproducible. If it still happens, please open a pull request with a failing test to confirm that. Thanks! |
Released: v0.40.0 🎉This has been released in v0.40.0! Make sure to always update to the latest version ( Predictable release automation by @ossjs/release. |
Fix Unix socket path handling in MSW Interceptors
Problem
The MSW Interceptors library had an issue with TCP requests bound to Unix sockets. When making requests to Unix socket paths (via the
socketPathoption), the interceptor was incorrectly trying to establish a TCP connection tolocalhost:80rather than using the provided socket path. This prevented proper interception and mocking of requests to Unix sockets.This issue is particularly important for:
/var/run/docker.sock)Solution
The solution involves a comprehensive approach to properly handle Unix socket paths throughout the request interceptor chain:
Modified URL creation for Unix sockets:
getUrlByRequestOptions.tsto create URLs with a special hostname"unix-socket-placeholder"for Unix socket requestslocalhostEnhanced socket connection handling:
MockHttpSocket.tsto directly create socket connections usingnet.createConnection({ path: socketPath })when a socket path is specifiedImproved agent configuration:
MockAgentandMockHttpsAgentclasses to properly handle and preserve thesocketPathpropertybaseUrlFromConnectionOptions.tsto use the same special hostname for consistencyCode organization improvements:
ClientRequest/index.tsto extract duplicate agent creation code into a helper methodsocketPathproperty is properly preserved throughout the request handling chainComprehensive test coverage:
normalizeClientRequestArgs.tsto verifysocketPathpreservationgetUrlByRequestOptions.tsto verify special hostname URL creationTesting
The fix has been thoroughly tested with:
All tests are now passing, confirming that Unix socket connections are properly handled.
Related Issues
This fix addresses issues encountered when working with applications that leverage Unix sockets for communication, particularly in containerized environments or when interacting with system services.
nock@14breakstestcontainersnock/nock#2839