Skip to content

Commit 06772ed

Browse files
authored
Merge pull request #367 from bsv-blockchain/f2sm2p-fixes
Improved error handling in AuthFetch
2 parents 25cdbfa + 364e3cf commit 06772ed

File tree

9 files changed

+1176
-75
lines changed

9 files changed

+1176
-75
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. The format
55
## Table of Contents
66

77
- [Unreleased](#unreleased)
8+
- [1.8.7 - 2025-10-22](#187---2025-10-22)
89
- [1.8.5 - 2025-10-21](#185---2025-10-21)
910
- [1.8.4 - 2025-10-20](#184---2025-10-20)
1011
- [1.8.3 - 2025-10-16](#183---2025-10-16)
@@ -166,6 +167,15 @@ All notable changes to this project will be documented in this file. The format
166167

167168
---
168169

170+
### [1.8.7] - 2025-10-22
171+
172+
### Fixed
173+
174+
- **Auth**: Peer transport now preserves detailed `SimplifiedFetchTransport` errors instead of replacing them with generic peer send failures.
175+
- Improved error handling when payments are involved
176+
177+
---
178+
169179
### [1.8.5] - 2025-10-21
170180

171181
### Added

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bsv/sdk",
3-
"version": "1.8.6",
3+
"version": "1.8.7",
44
"type": "module",
55
"description": "BSV Blockchain Software Development Kit",
66
"main": "dist/cjs/mod.js",

src/auth/Peer.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,8 @@ export class Peer {
149149

150150
try {
151151
await this.transport.send(generalMessage)
152-
} catch (error: any) {
153-
const e = new Error(
154-
`Failed to send message to peer ${peerSession.peerIdentityKey ?? 'unknown'
155-
}: ${String(error.message)}`
156-
)
157-
e.stack = error.stack
158-
throw e
152+
} catch (error: unknown) {
153+
this.propagateTransportError(peerSession.peerIdentityKey, error)
159154
}
160155
}
161156

@@ -215,11 +210,8 @@ export class Peer {
215210

216211
try {
217212
await this.transport.send(certRequestMessage)
218-
} catch (error: any) {
219-
throw new Error(
220-
`Failed to send certificate request message to peer ${peerSession.peerIdentityKey ?? 'unknown'
221-
}: ${String(error.message)}`
222-
)
213+
} catch (error: unknown) {
214+
this.propagateTransportError(peerSession.peerIdentityKey, error)
223215
}
224216
}
225217

@@ -424,6 +416,25 @@ export class Peer {
424416
this.onInitialResponseReceivedCallbacks.delete(callbackID)
425417
}
426418

419+
private propagateTransportError (peerIdentityKey: string | undefined, error: unknown): never {
420+
if (error instanceof Error) {
421+
if (peerIdentityKey != null) {
422+
const existingDetails = (error as any).details
423+
if (existingDetails != null && typeof existingDetails === 'object') {
424+
if (existingDetails.peerIdentityKey == null) {
425+
existingDetails.peerIdentityKey = peerIdentityKey
426+
}
427+
} else {
428+
(error as any).details = { peerIdentityKey }
429+
}
430+
}
431+
throw error
432+
}
433+
434+
const message = `Failed to send message to peer ${peerIdentityKey ?? 'unknown'}: ${String(error)}`
435+
throw new Error(message)
436+
}
437+
427438
/**
428439
* Handles incoming messages from the transport.
429440
*
@@ -734,12 +745,8 @@ export class Peer {
734745

735746
try {
736747
await this.transport.send(certificateResponse)
737-
} catch (error: any) {
738-
const errorMessage = error instanceof Error ? error.message : String(error)
739-
throw new Error(
740-
`Failed to send certificate response message to peer ${peerSession.peerIdentityKey ?? 'unknown'
741-
}: ${errorMessage}`
742-
)
748+
} catch (error: unknown) {
749+
this.propagateTransportError(peerSession.peerIdentityKey, error)
743750
}
744751
}
745752

src/auth/__tests/Peer.test.ts

Lines changed: 238 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { VerifiableCertificate } from '../../auth/certificates/VerifiableCertifi
77
import { MasterCertificate } from '../../auth/certificates/MasterCertificate.js'
88
import { getVerifiableCertificates } from '../../auth/utils/getVerifiableCertificates.js'
99
import { CompletedProtoWallet } from '../certificates/__tests/CompletedProtoWallet.js'
10+
import { SimplifiedFetchTransport } from '../../auth/transports/SimplifiedFetchTransport.js'
1011

1112
const certifierPrivKey = new PrivateKey(21)
1213
const alicePrivKey = new PrivateKey(22)
@@ -355,7 +356,7 @@ describe('Peer class mutual authentication and certificate exchange', () => {
355356
// console.error(e)
356357
})
357358
})
358-
})
359+
})
359360

360361
await alice.toPeer(Utils.toArray('Hello Bob!'))
361362
await bobReceivedGeneralMessage
@@ -364,6 +365,57 @@ describe('Peer class mutual authentication and certificate exchange', () => {
364365
expect(certificatesReceivedByBob).toEqual([aliceVerifiableCertificate])
365366
}, 15000)
366367

368+
describe('propagateTransportError', () => {
369+
const createPeerInstance = (): Peer => {
370+
const transport: Transport = {
371+
send: jest.fn(async (_message: AuthMessage) => {}),
372+
onData: async () => {}
373+
}
374+
return new Peer({} as WalletInterface, transport)
375+
}
376+
377+
it('adds peer identity details to existing errors', () => {
378+
const peer = createPeerInstance()
379+
const originalError = new Error('send failed')
380+
381+
let thrown: Error | undefined
382+
try {
383+
(peer as any).propagateTransportError('peer-public-key', originalError)
384+
} catch (error) {
385+
thrown = error as Error
386+
}
387+
388+
expect(thrown).toBe(originalError)
389+
expect((thrown as any).details).toEqual({ peerIdentityKey: 'peer-public-key' })
390+
})
391+
392+
it('preserves existing details when appending peer identity', () => {
393+
const peer = createPeerInstance()
394+
const originalError = new Error('existing details')
395+
;(originalError as any).details = { status: 503 }
396+
397+
let thrown: Error | undefined
398+
try {
399+
(peer as any).propagateTransportError('peer-public-key', originalError)
400+
} catch (error) {
401+
thrown = error as Error
402+
}
403+
404+
expect(thrown).toBe(originalError)
405+
expect((thrown as any).details).toEqual({
406+
status: 503,
407+
peerIdentityKey: 'peer-public-key'
408+
})
409+
})
410+
411+
it('wraps non-error values with a helpful message', () => {
412+
const peer = createPeerInstance()
413+
expect(() => (peer as any).propagateTransportError(undefined, 'timeout')).toThrow(
414+
'Failed to send message to peer unknown: timeout'
415+
)
416+
})
417+
})
418+
367419
it('Alice requests Bob to present his library card before lending him a book', async () => {
368420
const alicePubKey = (await walletA.getPublicKey({ identityKey: true }))
369421
.publicKey
@@ -818,4 +870,189 @@ describe('Peer class mutual authentication and certificate exchange', () => {
818870
expect(certificatesReceivedByAlice).toEqual([bobVerifiableCertificate])
819871
expect(certificatesReceivedByBob).toEqual([aliceVerifiableCertificate])
820872
}, 20000)
873+
874+
describe('Transport Error Handling', () => {
875+
const privKey = PrivateKey.fromRandom()
876+
877+
test('Should trigger "Failed to send message to peer" error with network failure', async () => {
878+
// Create a mock fetch that always fails
879+
const failingFetch = (jest.fn() as any).mockRejectedValue(new Error('Network connection failed'))
880+
881+
// Create a transport that will fail
882+
const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
883+
884+
// Create a peer with the failing transport
885+
const wallet = new CompletedProtoWallet(privKey)
886+
const peer = new Peer(wallet, transport)
887+
888+
// Register a dummy onData callback (required before sending)
889+
await transport.onData(async (message) => {
890+
// This won't be called due to network failure
891+
})
892+
893+
// Try to send a message to peer - this should fail and trigger the error
894+
try {
895+
await peer.toPeer([1, 2, 3, 4], '03abc123def456')
896+
fail('Expected error to be thrown')
897+
} catch (error: any) {
898+
expect(error.message).toContain('Network error while sending authenticated request')
899+
expect(error.message).toContain('Network connection failed')
900+
}
901+
}, 15000)
902+
903+
test('Should trigger error with connection timeout', async () => {
904+
// Create a fetch that times out
905+
const timeoutFetch = (jest.fn() as any).mockImplementation(() => {
906+
return new Promise((_, reject) => {
907+
setTimeout(() => {
908+
reject(new Error('Request timeout'))
909+
}, 100)
910+
})
911+
})
912+
913+
const transport = new SimplifiedFetchTransport('http://localhost:9999', timeoutFetch)
914+
const wallet = new CompletedProtoWallet(privKey)
915+
const peer = new Peer(wallet, transport)
916+
917+
await transport.onData(async (message) => {})
918+
919+
try {
920+
await peer.toPeer([5, 6, 7, 8], '03def789abc123')
921+
fail('Expected error to be thrown')
922+
} catch (error: any) {
923+
expect(error.message).toContain('Network error while sending authenticated request')
924+
expect(error.message).toContain('Request timeout')
925+
}
926+
}, 15000)
927+
928+
test('Should trigger error with DNS resolution failure', async () => {
929+
// Create a fetch that fails with DNS error
930+
const dnsFetch = (jest.fn() as any).mockRejectedValue({
931+
code: 'ENOTFOUND',
932+
errno: -3008,
933+
message: 'getaddrinfo ENOTFOUND nonexistent.domain'
934+
})
935+
936+
const transport = new SimplifiedFetchTransport('http://nonexistent.domain:3000', dnsFetch)
937+
const wallet = new CompletedProtoWallet(privKey)
938+
const peer = new Peer(wallet, transport)
939+
940+
await transport.onData(async (message) => {})
941+
942+
try {
943+
await peer.toPeer([9, 10, 11, 12], '03xyz987fed654')
944+
fail('Expected error to be thrown')
945+
} catch (error: any) {
946+
expect(error.message).toContain('Network error while sending authenticated request')
947+
expect(error.message).toContain('[object Object]')
948+
}
949+
}, 15000)
950+
951+
test('Should trigger error during certificate request send', async () => {
952+
// Create a failing fetch
953+
const failingFetch = (jest.fn() as any).mockRejectedValue(new Error('Connection reset by peer'))
954+
955+
const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
956+
const wallet = new CompletedProtoWallet(privKey)
957+
const peer = new Peer(wallet, transport)
958+
959+
await transport.onData(async (message) => {})
960+
961+
try {
962+
// Try to send a certificate request - this should also trigger the error
963+
await peer.requestCertificates({
964+
certifiers: ['03certifier123'],
965+
types: { 'type1': ['field1'] }
966+
}, '03abc123def456')
967+
fail('Expected error to be thrown')
968+
} catch (error: any) {
969+
expect(error.message).toContain('Network error while sending authenticated request')
970+
expect(error.message).toContain('Connection reset by peer')
971+
}
972+
}, 15000)
973+
974+
test('Should trigger error during certificate response send', async () => {
975+
// Create a failing fetch
976+
const failingFetch = (jest.fn() as any).mockRejectedValue(new Error('Socket hang up'))
977+
978+
const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
979+
const wallet = new CompletedProtoWallet(privKey)
980+
const peer = new Peer(wallet, transport)
981+
982+
await transport.onData(async (message) => {})
983+
984+
try {
985+
// Try to send a certificate response - this should also trigger the error
986+
await peer.sendCertificateResponse('03verifier123', [])
987+
fail('Expected error to be thrown')
988+
} catch (error: any) {
989+
expect(error.message).toContain('Network error while sending authenticated request')
990+
expect(error.message).toContain('Socket hang up')
991+
}
992+
}, 15000)
993+
994+
test('Should propagate network errors with proper details', async () => {
995+
// Create a fetch that throws a custom error
996+
const customError = new Error('Custom transport error')
997+
customError.stack = 'Custom stack trace'
998+
const failingFetch = (jest.fn() as any).mockRejectedValue(customError)
999+
1000+
const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
1001+
const wallet = new CompletedProtoWallet(privKey)
1002+
const peer = new Peer(wallet, transport)
1003+
1004+
await transport.onData(async (message) => {})
1005+
1006+
try {
1007+
await peer.toPeer([13, 14, 15, 16], '03peer123456')
1008+
fail('Expected error to be thrown')
1009+
} catch (error: any) {
1010+
// Should create a network error wrapping the original error
1011+
expect(error.message).toContain('Network error while sending authenticated request')
1012+
expect(error.message).toContain('Custom transport error')
1013+
expect(error.cause).toBeDefined()
1014+
expect(error.cause.message).toBe('Custom transport error')
1015+
}
1016+
}, 15000)
1017+
1018+
test('Should handle non-Error transport failures', async () => {
1019+
// Create a fetch that throws a non-Error object
1020+
const failingFetch = (jest.fn() as any).mockRejectedValue('String error message')
1021+
1022+
const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
1023+
const wallet = new CompletedProtoWallet(privKey)
1024+
const peer = new Peer(wallet, transport)
1025+
1026+
await transport.onData(async (message) => {})
1027+
1028+
try {
1029+
await peer.toPeer([17, 18, 19, 20], '03peer789abc')
1030+
fail('Expected error to be thrown')
1031+
} catch (error: any) {
1032+
// Should create network error for non-Error objects
1033+
expect(error.message).toContain('Network error while sending authenticated request')
1034+
expect(error.message).toContain('String error message')
1035+
}
1036+
}, 15000)
1037+
1038+
test('Should handle undefined peer identity gracefully', async () => {
1039+
// Create a failing fetch
1040+
const failingFetch = (jest.fn() as any).mockRejectedValue('Network failure')
1041+
1042+
const transport = new SimplifiedFetchTransport('http://localhost:9999', failingFetch)
1043+
const wallet = new CompletedProtoWallet(privKey)
1044+
const peer = new Peer(wallet, transport)
1045+
1046+
await transport.onData(async (message) => {})
1047+
1048+
try {
1049+
// Try to send to an undefined peer (this might happen in some edge cases)
1050+
await peer.toPeer([21, 22, 23, 24], undefined as any)
1051+
fail('Expected error to be thrown')
1052+
} catch (error: any) {
1053+
expect(error.message).toContain('Network error while sending authenticated request')
1054+
expect(error.message).toContain('Network failure')
1055+
}
1056+
}, 15000)
1057+
})
8211058
})

0 commit comments

Comments
 (0)