chore: upgrade Node.js runtime to v24 across codebase #419

Merged
jason.woltje merged 438 commits from fix/auth-frontend-remediation into main 2026-02-17 01:04:47 +00:00
3 changed files with 125 additions and 2 deletions
Showing only changes of commit 7d9c102c6d - Show all commits

View File

@@ -85,6 +85,7 @@ describe("ConnectionService", () => {
findUnique: vi.fn(),
findMany: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
},
},
},
@@ -208,6 +209,36 @@ describe("ConnectionService", () => {
})
);
});
it("should delete connection and throw error if request fails", async () => {
const mockAxiosResponse: AxiosResponse = {
data: mockRemoteIdentity,
status: 200,
statusText: "OK",
headers: {},
config: {} as never,
};
vi.spyOn(httpService, "get").mockReturnValue(of(mockAxiosResponse));
vi.spyOn(httpService, "post").mockReturnValue(
throwError(() => new Error("Connection refused"))
);
const createSpy = vi
.spyOn(prismaService.federationConnection, "create")
.mockResolvedValue(mockConnection);
const deleteSpy = vi
.spyOn(prismaService.federationConnection, "delete")
.mockResolvedValue(mockConnection);
await expect(service.initiateConnection(mockWorkspaceId, mockRemoteUrl)).rejects.toThrow(
"Failed to initiate connection"
);
expect(createSpy).toHaveBeenCalled();
expect(deleteSpy).toHaveBeenCalledWith({
where: { id: mockConnection.id },
});
});
});
describe("acceptConnection", () => {

View File

@@ -10,6 +10,7 @@ import {
NotFoundException,
UnauthorizedException,
ServiceUnavailableException,
BadRequestException,
} from "@nestjs/common";
import { HttpService } from "@nestjs/axios";
import { FederationConnectionStatus, Prisma } from "@prisma/client";
@@ -68,7 +69,7 @@ export class ConnectionService {
const signature = await this.signatureService.signMessage(request);
const signedRequest: ConnectionRequest = { ...request, signature };
// Send connection request to remote instance (fire-and-forget for now)
// Send connection request to remote instance
try {
await firstValueFrom(
this.httpService.post(`${remoteUrl}/api/v1/federation/incoming/connect`, signedRequest)
@@ -76,7 +77,16 @@ export class ConnectionService {
this.logger.log(`Connection request sent to ${remoteUrl}`);
} catch (error) {
this.logger.error(`Failed to send connection request to ${remoteUrl}`, error);
// Connection is still created in PENDING state, can be retried
// Delete the failed connection to prevent zombie connections in PENDING state
await this.prisma.federationConnection.delete({
where: { id: connection.id },
});
const errorMessage = error instanceof Error ? error.message : "Unknown error";
throw new BadRequestException(
`Failed to initiate connection to ${remoteUrl}: ${errorMessage}`
);
}
return this.mapToConnectionDetails(connection);

View File

@@ -0,0 +1,82 @@
# Issue #275: Fix silent connection initiation failures
## Objective
Fix silent connection initiation failures where HTTP errors are caught but success is returned to the user, leaving zombie connections in PENDING state forever.
## Location
`apps/api/src/federation/connection.service.ts:72-80`
## Problem
Current code:
```typescript
try {
await firstValueFrom(
this.httpService.post(`${remoteUrl}/api/v1/federation/incoming/connect`, signedRequest)
);
this.logger.log(`Connection request sent to ${remoteUrl}`);
} catch (error) {
this.logger.error(`Failed to send connection request to ${remoteUrl}`, error);
// Connection is still created in PENDING state, can be retried
}
return this.mapToConnectionDetails(connection);
```
Issues:
- Catches HTTP failures but returns success
- Connection stays in PENDING state forever
- Creates zombie connections
- User sees success message but connection actually failed
## Solution
1. Delete the failed connection from database
2. Throw exception with clear error message
3. User gets immediate feedback that connection failed
## Implementation
```typescript
try {
await firstValueFrom(
this.httpService.post(`${remoteUrl}/api/v1/federation/incoming/connect`, signedRequest)
);
this.logger.log(`Connection request sent to ${remoteUrl}`);
} catch (error) {
this.logger.error(`Failed to send connection request to ${remoteUrl}`, error);
// Delete the failed connection to prevent zombie connections
await this.prisma.federationConnection.delete({
where: { id: connection.id },
});
throw new BadRequestException(
`Failed to initiate connection to ${remoteUrl}: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
```
## Testing
Test scenarios:
1. Remote instance is unreachable - should throw exception and delete connection
2. Remote instance returns error - should throw exception and delete connection
3. Remote instance times out - should throw exception and delete connection
4. Remote instance returns success - should create connection in PENDING state
## Progress
- [ ] Create scratchpad
- [ ] Implement fix in connection.service.ts
- [ ] Add/update tests
- [ ] Run quality gates
- [ ] Commit changes
- [ ] Create PR
- [ ] Merge to develop
- [ ] Close issue #275