Skip to content

fix: properly handle Unix socket paths in interceptors#722

Closed
vinialbano wants to merge 2 commits intomswjs:mainfrom
vinialbano:fix-unix-socket-handling
Closed

fix: properly handle Unix socket paths in interceptors#722
vinialbano wants to merge 2 commits intomswjs:mainfrom
vinialbano:fix-unix-socket-handling

Conversation

@vinialbano
Copy link
Copy Markdown

@vinialbano vinialbano commented May 6, 2025

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 socketPath option), the interceptor was incorrectly trying to establish a TCP connection to localhost:80 rather than using the provided socket path. This prevented proper interception and mocking of requests to Unix sockets.

This issue is particularly important for:

  • Applications using Docker API (which often communicates through /var/run/docker.sock)
  • TestContainers and other containerization tools that leverage Unix sockets
  • Database connections that use Unix sockets (e.g., MySQL, PostgreSQL)
  • Custom IPC mechanisms based on Unix sockets

Solution

The solution involves a comprehensive approach to properly handle Unix socket paths throughout the request interceptor chain:

  1. Modified URL creation for Unix sockets:

    • Updated getUrlByRequestOptions.ts to create URLs with a special hostname "unix-socket-placeholder" for Unix socket requests
    • This prevents the library from attempting TCP connections to localhost
  2. Enhanced socket connection handling:

    • Updated MockHttpSocket.ts to directly create socket connections using net.createConnection({ path: socketPath }) when a socket path is specified
    • This bypasses the hostname/port mechanism for Unix socket connections
  3. Improved agent configuration:

    • Modified MockAgent and MockHttpsAgent classes to properly handle and preserve the socketPath property
    • Added support in baseUrlFromConnectionOptions.ts to use the same special hostname for consistency
  4. Code organization improvements:

    • Refactored ClientRequest/index.ts to extract duplicate agent creation code into a helper method
    • Ensured the socketPath property is properly preserved throughout the request handling chain
  5. Comprehensive test coverage:

    • Added regression tests verifying Unix socket connections work correctly
    • Added unit tests for normalizeClientRequestArgs.ts to verify socketPath preservation
    • Added tests for getUrlByRequestOptions.ts to verify special hostname URL creation

Testing

The fix has been thoroughly tested with:

  • Direct Unix socket connections to local test servers
  • GET and POST requests with various headers and body content
  • Error handling for non-existent sockets
  • Integration with the full interception pipeline

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.

@mikicho mikicho requested a review from kettanaito May 6, 2025 19:52
@vinialbano
Copy link
Copy Markdown
Author

vinialbano commented May 20, 2025

Hi @kettanaito, were you able to take a look at this by any chance?
Or @msutkowski, @marcosvega91, @timdeschryver

@mikicho
Copy link
Copy Markdown
Member

mikicho commented Jun 7, 2025

I'm wondering what the expected behavior is from this lib POV. Should we intercept them or automatically pass through?

@vinialbano
Copy link
Copy Markdown
Author

vinialbano commented Jun 7, 2025

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.

@kettanaito
Copy link
Copy Markdown
Member

Sorry, I didn't have the time to look into this. Will do my best to review this this month.

Copy link
Copy Markdown
Member

@kettanaito kettanaito left a comment

Choose a reason for hiding this comment

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

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 })
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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') {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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', () => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If possible, please adhere to the flat test case structure. Thanks!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 }),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So socketPath isn't annotated on RequestOptions by Node.js? That's a pity.

@kettanaito
Copy link
Copy Markdown
Member

@mikicho, correct me if I'm wrong, but socketPath seems like another way to construct a request. As a result, it performs a regular request that must be intercepted and allowed to be mocked like any other request.

@mikicho
Copy link
Copy Markdown
Member

mikicho commented Jun 8, 2025

@kettanaito I'm not familiar with this either. From what I understand, it is a Unix way to communicate between processes.
From a quick look, it seems like Node translates/overrides it to path, but I may be missing something.

@mikicho
Copy link
Copy Markdown
Member

mikicho commented Jun 9, 2025

@kettanaito @vinialbano I think the root cause is that we create a custom agent and then override the path property, which determines the type of the request (pipe/tcp), with null. It should be the socket path in order to create a pipe connection.
When I disable it or send agent: false in the options, it works like a charm:

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.

@kettanaito
Copy link
Copy Markdown
Member

@mikicho, I think you are right regarding the default agent. Nice find!

@kettanaito
Copy link
Copy Markdown
Member

Do we need any additional changes to support Unix socket paths after #731?

@kettanaito
Copy link
Copy Markdown
Member

kettanaito commented Oct 19, 2025

Update

I'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 test/modules/http/compliance/http-unix-socket.test.ts and they were always passing. I wonder if this issue addresses something that the suggested tests does not cover. cc @vinialbano

@kettanaito
Copy link
Copy Markdown
Member

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!

@kettanaito
Copy link
Copy Markdown
Member

Released: v0.40.0 🎉

This has been released in v0.40.0!

Make sure to always update to the latest version (npm i @mswjs/interceptors@latest) to get the newest features and bug fixes.


Predictable release automation by @ossjs/release.

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.

MSW seems not to work with kafka testcontainers

3 participants