Skip to content

Commit 09f6230

Browse files
fix(discord): recover existing thread on 160004 error (#280)
* fix(discord): recover existing thread on 160004 error (fixes #279) * Harden 160004 error matching, add tests, and add changeset --------- Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
1 parent 90cfb6a commit 09f6230

3 files changed

Lines changed: 104 additions & 14 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@chat-adapter/discord": patch
3+
---
4+
5+
Fix silent thread creation failure when Discord returns error code 160004 ("A thread has already been created for this message"). The adapter now recovers by reusing the existing thread instead of falling back to a standalone channel message.

packages/adapter-discord/src/index.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3968,3 +3968,70 @@ describe("mentionRoleIds handling", () => {
39683968
fetchSpy.mockRestore();
39693969
});
39703970
});
3971+
3972+
// ============================================================================
3973+
// createDiscordThread 160004 Recovery Tests
3974+
// ============================================================================
3975+
3976+
describe("createDiscordThread 160004 recovery", () => {
3977+
const adapter = createDiscordAdapter({
3978+
botToken: "test-token",
3979+
publicKey: testPublicKey,
3980+
applicationId: "test-app-id",
3981+
logger: mockLogger,
3982+
});
3983+
3984+
it("should recover when Discord returns 160004 (thread already exists)", async () => {
3985+
const { NetworkError } = await import("@chat-adapter/shared");
3986+
3987+
const spy = vi
3988+
.spyOn(adapter as any, "discordFetch")
3989+
.mockRejectedValue(
3990+
new NetworkError(
3991+
"discord",
3992+
'Discord API error: 400 {"code": 160004, "message": "A thread has already been created for this message"}'
3993+
)
3994+
);
3995+
3996+
const result = await (adapter as any).createDiscordThread(
3997+
"channel123",
3998+
"msg456"
3999+
);
4000+
4001+
expect(result.id).toBe("msg456");
4002+
expect(result.name).toContain("Thread ");
4003+
4004+
spy.mockRestore();
4005+
});
4006+
4007+
it("should propagate non-160004 NetworkErrors", async () => {
4008+
const { NetworkError } = await import("@chat-adapter/shared");
4009+
4010+
const spy = vi
4011+
.spyOn(adapter as any, "discordFetch")
4012+
.mockRejectedValue(
4013+
new NetworkError(
4014+
"discord",
4015+
'Discord API error: 403 {"code": 50001, "message": "Missing Access"}'
4016+
)
4017+
);
4018+
4019+
await expect(
4020+
(adapter as any).createDiscordThread("channel123", "msg456")
4021+
).rejects.toThrow(NetworkError);
4022+
4023+
spy.mockRestore();
4024+
});
4025+
4026+
it("should propagate non-NetworkError errors", async () => {
4027+
const spy = vi
4028+
.spyOn(adapter as any, "discordFetch")
4029+
.mockRejectedValue(new Error("Connection failed"));
4030+
4031+
await expect(
4032+
(adapter as any).createDiscordThread("channel123", "msg456")
4033+
).rejects.toThrow("Connection failed");
4034+
4035+
spy.mockRestore();
4036+
});
4037+
});

packages/adapter-discord/src/index.ts

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -959,23 +959,41 @@ export class DiscordAdapter implements Adapter<DiscordThreadId, unknown> {
959959
threadName,
960960
});
961961

962-
const response = await this.discordFetch(
963-
`/channels/${channelId}/messages/${messageId}/threads`,
964-
"POST",
965-
{
966-
name: threadName,
967-
auto_archive_duration: 1440, // 24 hours
968-
}
969-
);
962+
try {
963+
const response = await this.discordFetch(
964+
`/channels/${channelId}/messages/${messageId}/threads`,
965+
"POST",
966+
{
967+
name: threadName,
968+
auto_archive_duration: 1440, // 24 hours
969+
}
970+
);
970971

971-
const result = (await response.json()) as { id: string; name: string };
972+
const result = (await response.json()) as { id: string; name: string };
972973

973-
this.logger.debug("Discord API: POST thread response", {
974-
threadId: result.id,
975-
threadName: result.name,
976-
});
974+
this.logger.debug("Discord API: POST thread response", {
975+
threadId: result.id,
976+
threadName: result.name,
977+
});
977978

978-
return result;
979+
return result;
980+
} catch (error) {
981+
// Discord error 160004: "A thread has already been created for this message"
982+
// Recover by using the existing thread (its ID equals the parent message ID).
983+
if (
984+
error instanceof NetworkError &&
985+
typeof error.message === "string" &&
986+
error.message.includes('"code"') &&
987+
error.message.includes("160004")
988+
) {
989+
this.logger.debug(
990+
"Thread already exists for message, reusing existing thread",
991+
{ channelId, messageId }
992+
);
993+
return { id: messageId, name: threadName };
994+
}
995+
throw error;
996+
}
979997
}
980998

981999
/**

0 commit comments

Comments
 (0)