Merge pull request #3104 from fitzgpt/fix/non-ascii-directory-header
fix(server): percent-encode non-ASCII directory headers to OpenCode
This commit is contained in:
@@ -34,3 +34,7 @@ server implementation. VS Code does not call this route for workspace images;
|
|||||||
those use its local filesystem bridge. If called, the grant route returns an
|
those use its local filesystem bridge. If called, the grant route returns an
|
||||||
explicit unsupported response because OpenCode temporary images are not
|
explicit unsupported response because OpenCode temporary images are not
|
||||||
supported there.
|
supported there.
|
||||||
|
|
||||||
|
Requests to OpenCode carry the directory in a percent-encoded
|
||||||
|
`x-opencode-directory` header, matching the SDK wire format; OpenCode rejects
|
||||||
|
raw non-ASCII header values.
|
||||||
|
|||||||
@@ -200,7 +200,9 @@ const fetchMessage = async ({ sessionId, messageId, directory, buildOpenCodeUrl,
|
|||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
accept: 'application/json',
|
accept: 'application/json',
|
||||||
'x-opencode-directory': directory,
|
// Percent-encoded to match the SDK wire format; raw non-ASCII values
|
||||||
|
// are rejected by OpenCode.
|
||||||
|
'x-opencode-directory': encodeURIComponent(directory),
|
||||||
...getOpenCodeAuthHeaders(),
|
...getOpenCodeAuthHeaders(),
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(10_000),
|
signal: AbortSignal.timeout(10_000),
|
||||||
|
|||||||
@@ -74,6 +74,20 @@ const prepare = (app, directory, sources) => request(app)
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
describe('session image assets', () => {
|
describe('session image assets', () => {
|
||||||
|
it('percent-encodes the directory header on the message fetch', async () => {
|
||||||
|
const fixture = await createFixture();
|
||||||
|
await prepare(fixture.app, fixture.directory, ['image.png']);
|
||||||
|
|
||||||
|
expect(fixture.fetchMock).toHaveBeenCalledWith(
|
||||||
|
expect.any(URL),
|
||||||
|
expect.objectContaining({
|
||||||
|
headers: expect.objectContaining({
|
||||||
|
'x-opencode-directory': encodeURIComponent(fixture.directory),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('prepares workspace and OpenCode temporary images with one message fetch', async () => {
|
it('prepares workspace and OpenCode temporary images with one message fetch', async () => {
|
||||||
const fixture = await createFixture({ sources: ['workspace.png'] });
|
const fixture = await createFixture({ sources: ['workspace.png'] });
|
||||||
await fs.writeFile(path.join(fixture.directory, 'workspace.png'), PNG);
|
await fs.writeFile(path.join(fixture.directory, 'workspace.png'), PNG);
|
||||||
|
|||||||
@@ -83,7 +83,10 @@ const resolveVariant = (providers, providerID, modelID, variant) => {
|
|||||||
const parseConfigModel = (value) => splitModel(value);
|
const parseConfigModel = (value) => splitModel(value);
|
||||||
|
|
||||||
const buildDirectoryHeaders = (directory) => ({
|
const buildDirectoryHeaders = (directory) => ({
|
||||||
...(directory ? { 'x-opencode-directory': directory } : {}),
|
// OpenCode rejects non-ASCII header values; the official SDK sends this
|
||||||
|
// header percent-encoded, so match that wire format (non-ASCII checkout
|
||||||
|
// paths such as "Masaüstü" otherwise fail every dispatched prompt).
|
||||||
|
...(directory ? { 'x-opencode-directory': encodeURIComponent(directory) } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchJson = async (url, authHeaders, fallback, directory) => {
|
const fetchJson = async (url, authHeaders, fallback, directory) => {
|
||||||
|
|||||||
@@ -161,6 +161,29 @@ describe('openchamber session routes', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('percent-encodes the directory header for non-ASCII checkout paths', async () => {
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
globalThis.fetch = vi.fn(async () => ({ ok: true, json: async () => ({ id: 'ses_123' }) }));
|
||||||
|
try {
|
||||||
|
const { app } = createApp();
|
||||||
|
await request(app)
|
||||||
|
.post('/api/openchamber/sessions')
|
||||||
|
.send({ directory: '/home/user/Masaüstü/projeler', title: 'Side task' })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
headers: expect.objectContaining({
|
||||||
|
'x-opencode-directory': encodeURIComponent('/home/user/Masaüstü/projeler'),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('parses JSON body without global middleware', async () => {
|
it('parses JSON body without global middleware', async () => {
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
globalThis.fetch = vi.fn(async () => ({ ok: true, json: async () => ({ id: 'ses_123' }) }));
|
globalThis.fetch = vi.fn(async () => ({ ok: true, json: async () => ({ id: 'ses_123' }) }));
|
||||||
|
|||||||
Reference in New Issue
Block a user