Skip to content

Feature/fix blockwise requires message#626

Open
jkralik wants to merge 17 commits intomasterfrom
jkralik/fix/blockwise-requires-Message-ID
Open

Feature/fix blockwise requires message#626
jkralik wants to merge 17 commits intomasterfrom
jkralik/fix/blockwise-requires-Message-ID

Conversation

@jkralik
Copy link
Copy Markdown
Member

@jkralik jkralik commented Oct 8, 2025

Summary by CodeRabbit

  • New Features

    • Block-wise transfers now track exchanges per remote address for more reliable per-peer handling.
  • Refactor

    • Block-wise APIs and internal flows updated to propagate remote address; TCP and UDP clients adjusted accordingly.
  • Tests

    • Added UDP libcoap-based blockwise tests (GET/PUT/POST) with large payloads and test utilities.
  • Chores

    • Examples switched to UDP port 5685.
    • .gitignore adjusted to ignore only the repository root IDE directory (/.idea/).

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 8, 2025

Walkthrough

Block-wise transfer keying moved from token.Hash() to an exchangeKey (derived from token or remoteAddr). BlockWise APIs (Do, Handle) now accept a net.Addr and thread exchangeKey through cache/handlers. Client call-sites updated to pass RemoteAddr(). Misc: root .gitignore tightened, example UDP ports changed, libcoap tests and CI install steps added.

Changes

Cohort / File(s) Summary
Git ignore scope
/.gitignore
Changed ignore pattern from .idea/ to /.idea/, restricting the ignore to the repository root .idea directory.
Example ports
examples/simple/client/main.go, examples/simple/server/main.go
Updated example UDP port from :5688 to :5685 (client dial target and server ListenAndServe).
Blockwise exchangeKey integration & API changes
net/blockwise/blockwise.go
Replaced token.Hash()-based cache keys with an exchangeKey derived via getExchangeKey(token, addr); threaded exchangeKey (uint64) through internal helpers; updated Do and Handle signatures to accept remoteAddr net.Addr; added exchange-key-based register/get/cleanup flows and adjusted error paths.
Client call-site propagation (TCP)
tcp/client/conn.go
Updated blockWise.Do and blockWise.Handle calls to pass cc.RemoteAddr() into blockwise processing.
Client call-site propagation (UDP)
udp/client/conn.go
Updated blockWise.Do and blockWise.Handle calls to pass cc.RemoteAddr() into blockwise processing.
Blockwise tests (call adjustments)
net/blockwise/blockwise_test.go
Adjusted test call sites to include the additional nil/addr parameter before callbacks; call mechanics changed but assertions/behavior unchanged.
New UDP libcoap tests
udp/server_libcoap_test.go
Added libcoap-driven UDP blockwise tests (GET/PUT/POST), helper sendCoAPRequestLibCoAP, and bigPayload10KiB constant; tests require external coap-client binary.
CI: install coap-client
.github/workflows/test.yml, .github/workflows/test-for-fork.yml
Added steps to install libcoap/coap-client on ubuntu/macOS runners to support the new libcoap tests.
DTLS/TLS test return consistency
dtls/server_test.go, tcp/server_test.go
Adjusted helper functions to return consistent multi-value tuples on early error paths (propagate configs and error explicitly).
Router return consistency
mux/router.go
Router.Match updated to explicitly return (matchedRoute, matchedPattern) in all control paths (including nil/no-match).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor App
    participant ClientConn as ClientConn (TCP/UDP)
    participant BlockWise as BlockWise
    participant Keyer as getExchangeKey
    participant Cache as Exchange Cache

    App->>ClientConn: Do(req)
    ClientConn->>BlockWise: Do(req, maxSZX, maxMsgSize, RemoteAddr, doInternal)
    BlockWise->>Keyer: getExchangeKey(token, RemoteAddr)
    Keyer-->>BlockWise: exchangeKey / error
    alt exchangeKey OK
        BlockWise->>Cache: Lookup(exchangeKey)
        alt cached in-progress
            BlockWise-->>ClientConn: attach/wait
        else no cache
            BlockWise->>ClientConn: send blocks via doInternal
            ClientConn-->>BlockWise: responses
            BlockWise->>Cache: Update/Cleanup(exchangeKey)
        end
        BlockWise-->>App: final response
    else error
        BlockWise-->>App: error
    end
Loading
sequenceDiagram
    autonumber
    actor RemotePeer
    participant ServerConn as ServerConn
    participant BlockWise as BlockWise
    participant Keyer as getExchangeKey
    participant Cache as Exchange Cache
    participant Handler as next()

    RemotePeer->>ServerConn: Incoming message
    ServerConn->>BlockWise: Handle(w, r, maxSZX, maxMsgSize, RemoteAddr, next)
    BlockWise->>Keyer: getExchangeKey(r.Token, RemoteAddr)
    Keyer-->>BlockWise: exchangeKey / error
    alt exchangeKey OK
        BlockWise->>Cache: Get/Assemble(exchangeKey)
        BlockWise->>Handler: next(w, r)
        Handler-->>BlockWise: handled
        BlockWise->>Cache: Cleanup/Retain(exchangeKey)
    else error
        BlockWise-->>ServerConn: write error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay special attention to net/blockwise/blockwise.go for correctness of exchangeKey derivation, concurrency around cache entries, and correctness of new/changed function signatures.
  • Verify all call-sites (TCP/UDP clients, tests) correctly pass remoteAddr and that tests compile/run with the new signatures.
  • Review new libcoap tests and CI workflow additions for platform assumptions and external binary availability.

Possibly related PRs

Poem

I nibble bytes with whiskered grace,
I hash a token, then change my pace,
If tokens sleep, I ask the peer,
exchangeKey hops the cache right here.
Thump-thump — blocks hop home, a happy trace. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title "Feature/fix blockwise requires message" is vague and does not clearly convey the main purpose of the changeset. While it references "blockwise," which is indeed a central component of the changes, the phrase "requires message" is ambiguous and unclear. The title does not effectively communicate that the PR's primary objective is to refactor the blockwise implementation to accept a remote address parameter, derive an exchange key from either token hash or remote address for cache lookups, and enable RFC-compliant scenarios with empty tokens. A developer scanning git history would struggle to understand what this PR accomplishes without reading the full diff. Consider revising the title to be more specific and descriptive. A clearer title might be: "Add remoteAddr parameter to blockwise Do/Handle for exchange-key-based caching" or "Support exchange-key-based cache lookups in blockwise for empty-token scenarios." The revised title should clearly indicate both what is being changed (blockwise method signatures) and why (to support RFC-compliant empty-token scenarios with remote-address-derived keys).
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jkralik/fix/blockwise-requires-Message-ID

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 522b23e and 07c41fa.

📒 Files selected for processing (2)
  • .github/workflows/test-for-fork.yml (1 hunks)
  • .github/workflows/test.yml (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/test.yml
  • .github/workflows/test-for-fork.yml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Fuzzing
  • GitHub Check: test (macOS-latest)
  • GitHub Check: test (ubuntu-latest)
  • GitHub Check: test1_20

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
net/blockwise/blockwise.go (2)

289-300: Passing 0 for exchangeKey relies on implicit fallback logic.

The WriteMessage function passes 0 as the exchangeKey parameter to startSendingMessage. While this is handled correctly by startSendingMessage (lines 570-572), which computes the key from the token when it's 0, this implicit behavior could be confusing.

Consider making this more explicit by computing the exchangeKey here:

 func (b *BlockWise[C]) WriteMessage(request *pool.Message, maxSZX SZX, maxMessageSize uint32, writeMessage func(r *pool.Message) error) error {
 	startSendingMessageBlock, err := EncodeBlockOption(maxSZX, 0, true)
 	if err != nil {
 		return fmt.Errorf("cannot encode start sending message block option(%v,%v,%v): %w", maxSZX, 0, true, err)
 	}
 
 	w := newWriteRequestResponse(b.cc, request)
-	err = b.startSendingMessage(w, maxSZX, maxMessageSize, startSendingMessageBlock, 0)
+	exchangeKey := request.Token().Hash()
+	err = b.startSendingMessage(w, maxSZX, maxMessageSize, startSendingMessageBlock, exchangeKey)
 	if err != nil {
 		return fmt.Errorf("cannot start writing request: %w", err)
 	}
 	return writeMessage(w.Message())
 }

867-879: Consider hash collision implications for remote address keying.

The getExchangeKey function uses FNV-1a to hash the string representation of net.Addr. While FNV-1a is fast and provides reasonable distribution, there are considerations:

  1. Address string stability: The function relies on addr.String() being consistent. This is generally safe for net.Addr implementations, but could be fragile if custom address types have non-deterministic string representations.

  2. Hash collisions: With a 64-bit hash space, the probability of collision is low but non-zero. For a large number of concurrent connections from different endpoints, collisions could cause cross-contamination of blockwise state between different clients.

  3. Empty token semantics: According to RFC 7252, empty tokens are valid when "no other tokens are in use to a destination." The exchange key mechanism assumes one concurrent blockwise exchange per remote address when tokens are empty, which may not hold if the remote sends multiple concurrent requests with empty tokens.

Consider these enhancements:

  1. Document the limitations and assumptions in a comment above the function.
  2. For production scenarios with high connection counts, consider incorporating additional entropy (e.g., local address, connection ID) to reduce collision probability.
  3. Add collision detection/handling in cache operations if feasible.

Example documentation:

+// getExchangeKey returns a cache key for blockwise exchanges derived from the remote address.
+// This is used when the CoAP token is empty per RFC 7252 section 5.3.1.
+// 
+// Limitations:
+// - Relies on addr.String() being deterministic and unique per endpoint
+// - Hash collisions are possible but unlikely with 64-bit FNV-1a
+// - Assumes at most one concurrent empty-token blockwise exchange per remote address
 func getExchangeKey(addr net.Addr) (uint64, error) {
 
 	if addr == nil {
 		return 0, errors.New("cannot get exchange key: addr is nil")
 	}
 
 	h := fnv.New64a()
 	h.Write([]byte(addr.String()))
 	return h.Sum64(), nil
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f73dcbd and b4eb8a2.

📒 Files selected for processing (5)
  • .gitignore (1 hunks)
  • examples/simple/client/main.go (1 hunks)
  • net/blockwise/blockwise.go (19 hunks)
  • tcp/client/conn.go (2 hunks)
  • udp/client/conn.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
examples/simple/client/main.go (1)
udp/client.go (1)
  • Dial (25-40)
tcp/client/conn.go (2)
udp/client/conn.go (1)
  • Session (51-68)
tcp/client/session.go (1)
  • Session (22-43)
udp/client/conn.go (4)
message/pool/message.go (1)
  • Message (26-41)
net/responsewriter/responseWriter.go (1)
  • ResponseWriter (17-21)
tcp/client/conn.go (1)
  • Conn (44-57)
mux/client.go (1)
  • Conn (17-51)
net/blockwise/blockwise.go (4)
message/pool/message.go (1)
  • Message (26-41)
message/message.go (1)
  • Message (12-21)
pkg/cache/cache.go (2)
  • NewElement (32-39)
  • Element (14-18)
net/responsewriter/responseWriter.go (2)
  • New (23-38)
  • ResponseWriter (17-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test (windows-latest)
  • GitHub Check: test (macOS-latest)
  • GitHub Check: Fuzzing
🔇 Additional comments (9)
.gitignore (1)

14-14: LGTM!

The change from .idea/ to /.idea/ correctly narrows the ignore pattern to only the root .idea directory, preventing unintended ignores in subdirectories.

net/blockwise/blockwise.go (4)

8-10: LGTM!

The new imports hash/fnv and net are correctly added to support the exchange key mechanism based on remote addresses.


207-226: LGTM with note about empty token handling.

The Do function correctly computes the exchangeKey from the token hash and validates that the token is non-empty. This ensures consistent cache behavior.

Note: Unlike the Handle function which allows empty tokens (falling back to remote address), Do requires a valid token. This design is appropriate since Do is for client-initiated requests that always have tokens.


354-393: LGTM! Robust fallback mechanism for empty tokens.

The Handle function correctly implements a fallback mechanism:

  1. Uses token hash if token is non-empty (lines 360-362)
  2. Falls back to getExchangeKey(remoteAddr) if token is empty (lines 363-369)

This handles both standard CoAP messages with tokens and edge cases where tokens may be absent (as per RFC 7252).


546-580: LGTM! Handles zero exchangeKey correctly.

The function correctly handles the case where exchangeKey is 0 by computing it from the token (lines 570-572). This provides a sensible fallback for callers like WriteMessage that may not compute the key upfront.

tcp/client/conn.go (2)

210-210: LGTM!

The Do call correctly passes cc.RemoteAddr() to the updated blockwise API, enabling exchange key derivation from the remote address.


355-355: LGTM!

The Handle call correctly passes cc.RemoteAddr() to the updated blockwise API, enabling exchange key derivation from the remote address.

udp/client/conn.go (2)

443-443: LGTM!

The Do call correctly passes cc.RemoteAddr() to the updated blockwise API, enabling exchange key derivation from the remote address.


666-666: LGTM!

The Handle call correctly passes cc.RemoteAddr() to the updated blockwise API, enabling exchange key derivation from the remote address.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
net/blockwise/blockwise.go (1)

208-226: Critical: Unused remoteAddr parameter and inconsistent empty token handling.

The remoteAddr parameter is unused, and Do rejects empty tokens (line 213) despite RFC 7252 allowing them. This is inconsistent with Handle (lines 359-370), which derives an exchangeKey from remoteAddr when the token is empty.

Apply this diff to align Do with Handle and support empty tokens:

 func (b *BlockWise[C]) Do(r *pool.Message, maxSzx SZX, maxMessageSize uint32, remoteAddr net.Addr, do func(req *pool.Message) (*pool.Message, error)) (*pool.Message, error) {
 	if maxSzx > SZXBERT {
 		return nil, errors.New("invalid szx")
 	}
-	if len(r.Token()) == 0 {
-		return nil, errors.New("invalid token")
-	}
 
 	expire, ok := r.Context().Deadline()
 	if !ok {
 		expire = time.Now().Add(b.expiration)
 	}
 
-	exchangeKey := r.Token().Hash()
+	var exchangeKey uint64
+	if len(r.Token()) != 0 {
+		exchangeKey = r.Token().Hash()
+	} else {
+		var err error
+		exchangeKey, err = getExchangeKey(remoteAddr)
+		if err != nil {
+			return nil, fmt.Errorf("cannot get exchange key: %w", err)
+		}
+	}
+
 	_, loaded := b.sendingMessagesCache.LoadOrStore(exchangeKey, cache.NewElement(r, expire, nil))
 	if loaded {
-		return nil, errors.New("invalid token")
+		return nil, errors.New("message with the same exchange key already exists")
 	}
🧹 Nitpick comments (3)
net/blockwise/blockwise.go (3)

354-370: Remove unnecessary trailing newline.

The empty token handling logic is correct, but there's an unnecessary trailing newline at line 370.

Apply this diff:

 		exchangeKey, err = getExchangeKey(remoteAddr)
 		if err != nil {
 			b.errors(fmt.Errorf("cannot get exchange key from request Parameters(%v): %w", r, err))
 			return
 		}
-
 	}

As per static analysis.


582-583: Fix formatting issues.

The method signature is correct, but there are formatting issues: unnecessary leading newline and improper formatting.

Apply this diff:

-
-func (b *BlockWise[C]) getSentRequest(exchangeKey uint64, token message.Token) *pool.Message {
-
+func (b *BlockWise[C]) getSentRequest(exchangeKey uint64, token message.Token) *pool.Message {
 	data, ok := b.sendingMessagesCache.LoadWithFunc(exchangeKey, func(value *cache.Element[*pool.Message]) *cache.Element[*pool.Message] {

As per static analysis.


869-881: Remove unnecessary leading newline.

The getExchangeKey implementation is correct: FNV-1a provides a fast, non-cryptographic hash suitable for cache keys, and the nil check prevents panics. However, there's an unnecessary leading newline.

Apply this diff to fix formatting:

 }
-
 // getExchangeKey returns a key for the blockwise exchange cache.
 // According to RFC 7252: "[...] An empty token value is appropriate e.g., when no other tokens are in use to a destination"
 // so we need to generate a key from the remote address of the corresponding endpoint.
 func getExchangeKey(addr net.Addr) (uint64, error) {
-
 	if addr == nil {
 		return 0, errors.New("cannot get exchange key: addr is nil")
 	}
-
 	h := fnv.New64a()
 	h.Write([]byte(addr.String()))
 	return h.Sum64(), nil

As per static analysis.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4eb8a2 and 41461e6.

📒 Files selected for processing (2)
  • net/blockwise/blockwise.go (17 hunks)
  • net/blockwise/blockwise_test.go (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
net/blockwise/blockwise.go (4)
message/pool/message.go (1)
  • Message (26-41)
message/message.go (1)
  • Message (12-21)
pkg/cache/cache.go (2)
  • NewElement (32-39)
  • Element (14-18)
net/responsewriter/responseWriter.go (2)
  • New (23-38)
  • ResponseWriter (17-21)
net/blockwise/blockwise_test.go (3)
message/pool/message.go (1)
  • Message (26-41)
net/responsewriter/responseWriter.go (2)
  • New (23-38)
  • ResponseWriter (17-21)
mux/router.go (1)
  • ResponseWriter (15-20)
🪛 GitHub Actions: Golangci-lint
net/blockwise/blockwise.go

[error] 583-583: File is not properly formatted (gofumpt).

🪛 GitHub Check: lint
net/blockwise/blockwise.go

[failure] 208-208:
unused-parameter: parameter 'remoteAddr' seems to be unused, consider removing or renaming it as _ (revive)


[failure] 370-370:
unnecessary trailing newline (whitespace)


[failure] 582-582:
unnecessary leading newline (whitespace)


[failure] 583-583:
File is not properly formatted (gofumpt)


[failure] 872-872:
unnecessary leading newline (whitespace)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test (macOS-latest)
  • GitHub Check: test (windows-latest)
  • GitHub Check: Fuzzing
🔇 Additional comments (4)
net/blockwise/blockwise_test.go (1)

80-80: LGTM!

Test updates correctly accommodate the new remoteAddr parameter in Handle and Do signatures. Using nil as a placeholder in tests is appropriate for this context.

Also applies to: 83-83, 302-302, 499-499, 505-505

net/blockwise/blockwise.go (3)

8-8: LGTM!

New imports are properly utilized: hash/fnv for generating exchange keys from addresses, and net for the net.Addr parameter.

Also applies to: 10-10


546-580: LGTM!

The fallback logic to derive exchangeKey from the token when it's zero (line 570-572) is correct and maintains compatibility. The rest of the function properly integrates the exchangeKey-based caching.


756-867: LGTM!

The method correctly propagates the exchangeKey parameter throughout all cache operations, including proper handling of observe responses that require a new exchange key (line 793).

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Oct 8, 2025

Codecov Report

❌ Patch coverage is 69.81132% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.27%. Comparing base (f73dcbd) to head (b66b2d0).

Files with missing lines Patch % Lines
net/blockwise/blockwise.go 70.58% 22 Missing and 8 partials ⚠️
tcp/client/conn.go 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #626      +/-   ##
==========================================
+ Coverage   73.17%   73.27%   +0.10%     
==========================================
  Files          73       73              
  Lines        6986     7031      +45     
==========================================
+ Hits         5112     5152      +40     
- Misses       1489     1493       +4     
- Partials      385      386       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
net/blockwise/blockwise.go (1)

577-599: Fix formatting issues.

The function logic correctly uses exchangeKey for cache lookup, but there are formatting issues flagged by the linter.

Lines 577-578 have formatting issues:

  • Line 577: Unnecessary leading newline
  • Line 578: File is not properly formatted

Apply this diff to fix:

-
 func (b *BlockWise[C]) getSentRequest(exchangeKey uint64, token message.Token) *pool.Message {
-
 	data, ok := b.sendingMessagesCache.LoadWithFunc(exchangeKey, func(value *cache.Element[*pool.Message]) *cache.Element[*pool.Message] {
🧹 Nitpick comments (2)
net/blockwise/blockwise.go (2)

207-229: Consider refactoring to reduce cyclomatic complexity.

The function logic correctly integrates remoteAddr to compute exchangeKey for cache operations. Error handling is appropriate.

The cyclomatic complexity (16) slightly exceeds the threshold (15). Consider extracting some of the block-handling logic into helper functions to improve maintainability:

  • Extract the payload size validation and block option encoding (lines 233-258)
  • Extract the buffer read logic (lines 259-276)

This would make the function easier to test and maintain, though the current implementation is still readable.


541-575: Consider the robustness of using 0 as a sentinel value.

The logic correctly handles the WriteMessage case by treating exchangeKey == 0 as "compute from token." However, there's a theoretical edge case.

The FNV-1a hash could theoretically return 0 for a remote address (probability: 1 in 2^64). If this occurs, lines 565-567 would incorrectly recompute the key using token.Hash().

While extremely unlikely, a more robust approach would be to use a sentinel value outside the valid hash range (e.g., maxUint64) or pass a boolean flag indicating whether to compute from token. However, given the low probability and the limited impact (cache key mismatch), the current implementation is acceptable for practical purposes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 32e9adc and fdc0de5.

📒 Files selected for processing (1)
  • net/blockwise/blockwise.go (17 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
net/blockwise/blockwise.go (3)
message/message.go (1)
  • Message (12-21)
message/getToken.go (1)
  • Token (9-9)
pkg/cache/cache.go (2)
  • NewElement (32-39)
  • Element (14-18)
🪛 GitHub Actions: Golangci-lint
net/blockwise/blockwise.go

[error] 208-208: golangci-lint: cyclomatic complexity 16 of func (*BlockWise).Do is high (>15) (gocyclo)

🪛 GitHub Check: lint
net/blockwise/blockwise.go

[failure] 208-208:
cyclomatic complexity 16 of func (*BlockWise).Do is high (> 15) (gocyclo)


[failure] 371-371:
shadow: declaration of "err" shadows declaration at line 362 (govet)


[failure] 577-577:
unnecessary leading newline (whitespace)


[failure] 578-578:
File is not properly formatted (gofumpt)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test (windows-latest)
  • GitHub Check: Fuzzing
🔇 Additional comments (4)
net/blockwise/blockwise.go (4)

8-10: LGTM: Necessary imports for the new functionality.

The hash/fnv import is used for hashing remote addresses when tokens are empty, and net import is needed for the net.Addr parameter in the updated API.


751-862: LGTM: Correct handling of exchangeKey propagation.

The function correctly uses exchangeKey for cache operations throughout. The special handling for observe responses (lines 782-789) is appropriate—when a new token is generated for observation, the exchangeKey is recomputed using token.Hash(), which ensures proper cache indexing for the subsequent GET requests.


864-879: Exchange key hashing is safe
net.Addr.String() yields a stable, unique IP:port for TCP/UDP/DTLS connections. No changes required.


292-304: All BlockWise.WriteMessage callers use non-empty tokens
Verified that every client request is created via AcquireMessage + SetToken(...) (e.g. in NewGet/Post/Put/Delete/Observe), so exchangeKey=0 correctly computes from a non-nil token. No changes needed.

@Theoyor
Copy link
Copy Markdown
Contributor

Theoyor commented Oct 9, 2025

Hi @jkralik, thanks for fixing the tests. Does anything else still needs to be done for it to be complete? Potentially a unit test with no tokens but remoteAddr set

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
net/blockwise/blockwise.go (3)

207-216: Improve error message clarity.

The error messages are too generic and don't guide users on what's wrong:

  • Line 210: "invalid szx" → should specify the valid range (0-7 or up to SZXBERT)
  • Line 213: "invalid token" → should explain the constraint (e.g., "empty token requires valid remoteAddr" if that becomes supported)

Apply this diff to improve the messages:

 func validateDoInput(r *pool.Message, maxSzx SZX) error {
 	if maxSzx > SZXBERT {
-		return errors.New("invalid szx")
+		return fmt.Errorf("invalid szx: must be <= SZXBERT (%d), got %d", SZXBERT, maxSzx)
 	}
 	if len(r.Token()) == 0 {
-		return errors.New("invalid token")
+		return errors.New("empty token not supported in Do method")
 	}
 	return nil
 }

619-621: Clarify fallback logic or remove if unnecessary.

The fallback to token.Hash() when exchangeKey is 0 suggests there are code paths where exchangeKey might not be provided. However, looking at the callers of startSendingMessage:

  • Line 353 in WriteMessage: passes 0 explicitly
  • Line 473 in handleReceivedMessage: passes the computed exchangeKey

The explicit 0 from WriteMessage seems to be the intended use case. Please add a comment explaining this fallback behavior:

+	// If exchangeKey is 0 (e.g., from WriteMessage), fall back to token-based key.
+	// This maintains compatibility with scenarios where remote address tracking isn't needed.
 	if exchangeKey == 0 {
 		exchangeKey = sendingMessage.Token().Hash()
 	}

917-932: Well-implemented exchange key derivation.

The function correctly handles both non-empty tokens (using token hash) and empty tokens (using address hash), aligning with RFC 7252's guidance that empty tokens are valid "when no other tokens are in use to a destination."

Minor observation on hash collisions:

Using FNV-1a hash of the address string is simple and practical. While hash collisions are theoretically possible (especially if address strings format inconsistently), they're unlikely in typical deployments. If stricter collision avoidance is needed in the future, consider:

  1. Using the raw address bytes instead of the string representation
  2. Combining address and port as separate hash inputs
  3. Using a cryptographic hash (though FNV is sufficient for cache keys)

For now, the current implementation is acceptable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fdc0de5 and ca1197e.

📒 Files selected for processing (1)
  • net/blockwise/blockwise.go (19 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
net/blockwise/blockwise.go (5)
message/pool/message.go (1)
  • Message (26-41)
net/responsewriter/responseWriter.go (2)
  • New (23-38)
  • ResponseWriter (17-21)
message/getToken.go (1)
  • Token (9-9)
pkg/cache/cache.go (2)
  • NewElement (32-39)
  • Element (14-18)
message/option.go (5)
  • Size1 (77-77)
  • Block1 (73-73)
  • Block2 (72-72)
  • Size2 (74-74)
  • OptionID (27-27)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test (windows-latest)
  • GitHub Check: Fuzzing
🔇 Additional comments (1)
net/blockwise/blockwise.go (1)

425-425: Variable shadowing fixed.

Good fix! The previous linter error about shadowing err at this line has been resolved by using err = instead of err :=.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Oct 9, 2025

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
net/blockwise/blockwise.go (1)

800-807: Remove early return for empty tokens in processReceivedMessage.
This breaks blockwise assembly when tokens are empty, despite the new exchangeKey support via remoteAddr. With empty tokens, we should still process using exchangeKey.

Apply this diff:

 func (b *BlockWise[C]) processReceivedMessage(w *responsewriter.ResponseWriter[C], r *pool.Message, maxSzx SZX, next func(w *responsewriter.ResponseWriter[C], r *pool.Message), exchangeKey uint64, blockType message.OptionID, sizeType message.OptionID) error {
   token := r.Token()
-  if len(token) == 0 {
-    next(w, r)
-    return nil
-  }
   if r.Code() == codes.GET || r.Code() == codes.DELETE {
     next(w, r)
     return nil
   }
♻️ Duplicate comments (2)
net/blockwise/blockwise.go (2)

290-302: Add early remoteAddr validation in Do when token is empty.
Avoid relying on a later getExchangeKey failure; return a clear error upfront.

Apply this diff:

 func (b *BlockWise[C]) Do(r *pool.Message, maxSzx SZX, maxMessageSize uint32, remoteAddr net.Addr, do func(req *pool.Message) (*pool.Message, error)) (*pool.Message, error) {
   // Input validation
   if err := validateDoInput(maxSzx); err != nil {
     return nil, err
   }
+  // Require remoteAddr when token is empty
+  if len(r.Token()) == 0 && remoteAddr == nil {
+    return nil, errors.New("token required unless remoteAddr is set")
+  }
 
   // Register sending message (token uniqueness + expiration)
   exchangeKey, err := b.registerSendingMessage(r, remoteAddr)
   if err != nil {
     return nil, err
   }

407-417: Add early remoteAddr validation in Handle when token is empty.
Mirror Do’s behavior for clearer error handling before getExchangeKey.

Apply this diff:

 func (b *BlockWise[C]) Handle(w *responsewriter.ResponseWriter[C], r *pool.Message, maxSZX SZX, maxMessageSize uint32, remoteAddr net.Addr, next func(w *responsewriter.ResponseWriter[C], r *pool.Message)) {
   if maxSZX > SZXBERT {
     panic("invalid maxSZX")
   }
+  if len(r.Token()) == 0 && remoteAddr == nil {
+    b.errors(errors.New("remoteAddr cannot be nil when token is empty"))
+    return
+  }
 
   exchangeKey, err := getExchangeKey(r.Token(), remoteAddr)
   if err != nil {
     b.errors(fmt.Errorf("cannot get exchange key from request Parameters(%v): %w", r, err))
     return
   }
🧹 Nitpick comments (4)
net/blockwise/blockwise.go (4)

223-235: Clarify collision error message in registerSendingMessage.
When LoadOrStore finds an existing entry, “invalid token” is misleading now that exchangeKey may come from remoteAddr. Prefer an error that reflects an exchangeKey collision.

Apply this diff:

 func (b *BlockWise[C]) registerSendingMessage(r *pool.Message, remoteAddr net.Addr) (uint64, error) {
   expire := b.computeExpire(r)
   exchangeKey, err := getExchangeKey(r.Token(), remoteAddr)
   if err != nil {
     return 0, fmt.Errorf("cannot get exchange key from request Parameters(%v): %w", r, err)
   }
   _, loaded := b.sendingMessagesCache.LoadOrStore(exchangeKey, cache.NewElement(r, expire, nil))
   if loaded {
-    return 0, errors.New("invalid token")
+    return 0, fmt.Errorf("duplicate exchange key (token=%x, addr=%v): another request is already in progress", r.Token(), remoteAddr)
   }
   return exchangeKey, nil
 }

554-577: Fix stale error text referencing token in continueSendingMessage.
Lookup is by exchangeKey. Error should reflect that for accurate diagnostics.

Apply this diff:

   if err == nil && sendMessage == nil {
-    err = fmt.Errorf("cannot find sending message for token(%v)", r.Token())
+    err = fmt.Errorf("cannot find sending message for exchangeKey(%d)", exchangeKey)
   }

628-649: Minor: naming nit in getSentRequest.
Parameter is an exchangeKey; consider renaming in a follow-up for clarity. No functional change needed.


914-929: getExchangeKey approach is reasonable; add minor guard if desired.
FNV-1a over remoteAddr.String() is fine for cache keys. Optionally, guard against empty remoteAddr.String().

Apply this diff:

 func getExchangeKey(token message.Token, remoteAddr net.Addr) (uint64, error) {
   if len(token) != 0 {
     return token.Hash(), nil
   }
 
   if remoteAddr == nil {
     return 0, errors.New("cannot get exchange key: remoteAddr is nil")
   }
 
   h := fnv.New64a()
-  h.Write([]byte(remoteAddr.String()))
+  addr := remoteAddr.String()
+  if addr == "" {
+    return 0, errors.New("cannot get exchange key: remoteAddr string is empty")
+  }
+  _, _ = h.Write([]byte(addr))
   return h.Sum64(), nil
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca1197e and b66b2d0.

📒 Files selected for processing (1)
  • net/blockwise/blockwise.go (19 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
net/blockwise/blockwise.go (5)
net/responsewriter/responseWriter.go (2)
  • New (23-38)
  • ResponseWriter (17-21)
message/pool/message.go (1)
  • Message (26-41)
message/getToken.go (1)
  • Token (9-9)
pkg/cache/cache.go (2)
  • NewElement (32-39)
  • Element (14-18)
message/option.go (5)
  • Size1 (77-77)
  • Block1 (73-73)
  • Block2 (72-72)
  • Size2 (74-74)
  • OptionID (27-27)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test (macOS-latest)
  • GitHub Check: test (windows-latest)
  • GitHub Check: Fuzzing
🔇 Additional comments (7)
net/blockwise/blockwise.go (7)

8-11: Imports look correct for new exchangeKey implementation.
FNV and net imports are appropriate for hashing remoteAddr and passing it through APIs.


116-130: OK: error propagation in DecodeBlockOption.
Returning the error explicitly improves clarity; no issues.


207-213: Good: token check removed from Do validation.
This aligns with allowing empty tokens when remoteAddr is provided.


215-222: computeExpire helper is sound.
Respects context deadlines; falls back to BlockWise expiration.


237-289: First-block builder looks correct.
Validates methods, sets Size1/Block1, handles body read/seek and releases on error. No issues.


592-626: startSendingMessage exchangeKey fallback is fine.
Using token.Hash() when exchangeKey is 0 preserves legacy behavior for server-initiated paths (e.g., WriteMessage).


290-328: Ensure all Do/Handle invocations supply a non-nil remoteAddr
Manually verify that every call to BlockWise[…].Do(...) and .Handle(...) passes a valid remoteAddr (none should be nil).

@jkralik
Copy link
Copy Markdown
Member Author

jkralik commented Oct 9, 2025

Hi @jkralik, thanks for fixing the tests. Does anything else still needs to be done for it to be complete? Potentially a unit test with no tokens but remoteAddr set

Hi @Theoyor,
Could you please create tests in UDP for the POST, PUT, GET, and Observe methods with the empty token case?
Also, could you add some tests using the third-party library libCoAP, which is commonly used by IoT devices?

@Theoyor
Copy link
Copy Markdown
Contributor

Theoyor commented Oct 9, 2025

Sure, do you mean the compiled "coap-client" of libcoap? Is it already present in the testing environment?

@jkralik
Copy link
Copy Markdown
Member Author

jkralik commented Oct 9, 2025

Sure, do you mean the compiled "coap-client" of libcoap? Is it already present in the testing environment?

No, we need to create it.

@Theoyor
Copy link
Copy Markdown
Contributor

Theoyor commented Oct 9, 2025

@jkralik I am not sure if I understand what you mean by "in UDP". Are there any tests for "with token" that I can use as reference to write mine and to know how to structure them as closely to the existing ones? Or should I do it in the blockwise_test.go file?
Should I test for blockwise and non-blockwise?

@jkralik
Copy link
Copy Markdown
Member Author

jkralik commented Oct 9, 2025

@jkralik I am not sure if I understand what you mean by "in UDP". Are there any tests for "with token" that I can use as reference to write mine and to know how to structure them as closely to the existing ones? Or should I do it in the blockwise_test.go file? Should I test for blockwise and non-blockwise?

I mean github.com/plgd-dev/go-coap/v3/udp package. eg https://github.com/plgd-dev/go-coap/blob/master/udp/client_test.go#L32

@Theoyor
Copy link
Copy Markdown
Contributor

Theoyor commented Oct 13, 2025

@jkralik I implemented a test using libcoap for GET PUT POST. DELETE is not used with a payload at all so I would leave it out. The tests were successful after a small fix.

I am not sure whether writing tests using cc.NewGetRequest(...) etc. would make any sense, as it doesn't support the option of no token (and doesn't need to). Still remaining is "Observe", which I will now start.

Do you think it is also necessary to include non-token tests into blockwise_test.go as well?

@jkralik
Copy link
Copy Markdown
Member Author

jkralik commented Oct 14, 2025

@jkralik I implemented a test using libcoap for GET PUT POST. DELETE is not used with a payload at all so I would leave it out. The tests were successful after a small fix.

I am not sure whether writing tests using cc.NewGetRequest(...) etc. would make any sense, as it doesn't support the option of no token (and doesn't need to). Still remaining is "Observe", which I will now start.

Do you think it is also necessary to include non-token tests into blockwise_test.go as well?

It’s not necessary to include non-token tests in blockwise_test.go, as they are already covered through UDP.

@Theoyor
Copy link
Copy Markdown
Contributor

Theoyor commented Oct 15, 2025

So I found out that it seems coap observe does not work with blockwise as intended, neither token nor no-token. I will further dig into that

@Theoyor
Copy link
Copy Markdown
Contributor

Theoyor commented Oct 15, 2025

@jkralik ok. I did some research and testing.

Let's start with what the standards say:

For Blockwise observe WITH token: https://datatracker.ietf.org/doc/html/rfc7959#section-3.4

the server first sends a direct response to the initial GET request (the resulting block-wise transfer is as in Figure 4 and has therefore been left out). The second transfer is started by a 2.05 notification that contains just the first block; the client then goes on to obtain the rest of the blocks starting from the second block.[...] Note [...]that the requests for additional blocks cannot make use of the token of the Observation relationship

For Blockwise observe WITHOUT token:

What I found out during tests:

  • Not one tool from the three I tested can handle blockwise + observe correctly
    • aiocoap doesn't implement it and fails with "notImplementedError"
    • californiums cf-client doesn't send an observe and doesn't specify an option
    • libcoap coap-client comes closest BUT uses the same token for the initial observe and follow up GET for the 2nd and onward block
  • go-coap cannot deal with obs + block WITHOUT token
  • When used with token the following two issues occur (tested with libcoap which keeps the token)
    • The payload is transmitted by the server starting with first block after notification (should be the second)
    • The observe is aborted after the first body is sent
    • both issues might be due to the same token being used by libcoap but not sure.

My coclusion

  • Blockwise + observe doesn't work in either version of go-coap
  • The new change with exchangeKey doesn't change current behaviour in Blockwise + observe (I tested with coap-client using both master and my PR branch)
  • No major testing tool supports Blockwise + observe
  • I am not too deep into the go-coap code that I could change anything

My Proposal: I do a PR with the unit tests using coap-client onto thios branch here (so you can still modify before its merged onto master). Then we have blockwise without token working. Blcokwise + observe remains unchanged and untested. This should then be a different issue and PR.

What do you think?

@Theoyor
Copy link
Copy Markdown
Contributor

Theoyor commented Oct 15, 2025

I created #627 for unit tests please take a look. It merges into this branch so you can make changes before it is merged to master

Also one would need to add coap-client into the test environment, which I cannot do.

@Theoyor
Copy link
Copy Markdown
Contributor

Theoyor commented Oct 22, 2025

@jkralik any opinion?

* feat: unuttests using libcoap coapo-client. GET works with and without token, post and put only with token.

* fix: libcoap tests successful, fixed blockwise

* fix: cleanup

* fix: remove fixed ports from tests

---------

Co-authored-by: Theodor Rauch <theodor.rauch@ml-pa.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
net/blockwise/blockwise.go (1)

598-604: Use <= threshold to avoid unnecessary blockwise on exact-size payloads.

Keep behavior consistent with Do (which uses <=).

- if payloadSize < maxSZX.Size() {
+ if payloadSize <= maxSZX.Size() {
   return nil
 }
🧹 Nitpick comments (18)
udp/server_libcoap_test.go (14)

96-103: Remove unused client dial.

The Dialed UDP client connection isn’t used; drop it to speed up tests and reduce noise.

- cc, err := Dial(l.LocalAddr().String())
- require.NoError(t, err)
- defer func() {
-   errC := cc.Close()
-   require.NoError(t, errC)
-   <-cc.Done()
- }()

111-116: Fix indent-error-flow (revive): drop else after return.

Simplify flow to satisfy linter.

- if tt.wantErr {
-   require.Error(t, err)
-   return
- } else {
-   require.NoError(t, err)
- }
+ if tt.wantErr {
+   require.Error(t, err)
+   return
+ }
+ require.NoError(t, err)

77-83: Align content-format with expectations.

Handler sets AppOctets but test table lists TextPlain; either assert the content-format or switch to TextPlain for clarity.

- errS := w.SetResponse(codes.Content, message.AppOctets, bytes.NewReader([]byte(bigPayload10KiB)))
+ errS := w.SetResponse(codes.Content, message.TextPlain, bytes.NewReader([]byte(bigPayload10KiB)))

165-169: Skip on missing coap-client (PUT).

Mirror the skip behavior here too.

- _, err := exec.LookPath("coap-client")
- if err != nil {
-   t.Error("coap-client not found in PATH")
-   return
- }
+ if _, err := exec.LookPath("coap-client"); err != nil {
+   t.Skipf("skipping: coap-client not found in PATH")
+ }

183-191: Fix err shadowing (govet).

Avoid shadowing outer err when reading body.

- received, err := r.ReadBody()
- assert.NoError(t, err)
+ received, readErr := r.ReadBody()
+ assert.NoError(t, readErr)

216-221: Fix indent-error-flow (revive) in PUT test loop.

- if tt.wantErr {
-   require.Error(t, err)
-   return
- } else {
-   require.NoError(t, err)
- }
+ if tt.wantErr {
+   require.Error(t, err)
+   return
+ }
+ require.NoError(t, err)

264-269: Skip on missing coap-client (POST).

Mirror the skip behavior here as well.

- _, err := exec.LookPath("coap-client")
- if err != nil {
-   t.Error("coap-client not found in PATH")
-   return
- }
+ if _, err := exec.LookPath("coap-client"); err != nil {
+   t.Skipf("skipping: coap-client not found in PATH")
+ }

281-290: Fix err shadowing (govet) in POST handler.

- received, err := r.ReadBody()
+ received, readErr := r.ReadBody()
- assert.NoError(t, err)
+ assert.NoError(t, readErr)

315-320: Fix indent-error-flow (revive) in POST test loop.

- if tt.wantErr {
-   require.Error(t, err)
-   return
- } else {
-   require.NoError(t, err)
- }
+ if tt.wantErr {
+   require.Error(t, err)
+   return
+ }
+ require.NoError(t, err)

333-336: Prefer errors.New for static error text.

No formatting args used.

- return nil, fmt.Errorf("coap-client not found in PATH")
+ return nil, errors.New("coap-client not found in PATH")

341-346: Generate token once and use strconv.FormatUint (perfsprint).

Avoid double rand call; format efficiently.

- if token {
-   rand.Uint64()
-   args = append(args, "-T", fmt.Sprintf("%d", rand.Uint64()))
- }
+ if token {
+   tok := rand.Uint64()
+   args = append(args, "-T", strconv.FormatUint(tok, 10))
+ }

(Optional: seed math/rand or use crypto/rand for better randomness.)


345-347: Avoid redundant conversion.

bigPayload10KiB is already a string.

- args = append(args, "-e", string(bigPayload10KiB))
+ args = append(args, "-e", bigPayload10KiB)

26-33: Table fields unused/inconsistent; assert what you specify.

wantCode and wantContentFormat are never asserted; GET’s wantPayload type mismatches checks. Either add assertions (parse coap-client output) or drop the fields to avoid confusion.

Would you like me to add minimal parsing to assert response code/content-format from coap-client output?

Also applies to: 35-57, 137-163, 235-262


3-17: Add command timeout to avoid hangs.

Use exec.CommandContext with a short timeout when invoking external tools.

+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
- cmd := exec.Command(coapClientPath, args...)
+ cmd := exec.CommandContext(ctx, coapClientPath, args...)

Also applies to: 337-357, 359-363

net/blockwise/blockwise.go (4)

291-301: Validate remoteAddr early in Do.

Return a clear error before attempting register/getExchangeKey.

 if err := validateDoInput(maxSzx); err != nil {
   return nil, err
 }
+ if remoteAddr == nil {
+   return nil, errors.New("remoteAddr cannot be nil")
+ }

408-417: Validate remoteAddr early in Handle.

Short-circuit with a clear error when remoteAddr is nil and token is empty.

 func (b *BlockWise[C]) Handle(w *responsewriter.ResponseWriter[C], r *pool.Message, maxSZX SZX, maxMessageSize uint32, remoteAddr net.Addr, next func(w *responsewriter.ResponseWriter[C], r *pool.Message)) {
   if maxSZX > SZXBERT {
     panic("invalid maxSZX")
   }
+  if remoteAddr == nil && len(r.Token()) == 0 {
+    b.errors(errors.New("cannot get exchange key: remoteAddr is nil and token is empty"))
+    return
+  }
 
   exchangeKey, err := getExchangeKey(r.Token(), remoteAddr)

231-233: Clarify error on duplicate in-flight request.

“invalid token” is misleading when exchangeKey is address-derived; make the message explicit.

- return 0, errors.New("invalid token")
+ return 0, errors.New("duplicate exchangeKey: in-flight request already registered")

574-579: Reference exchangeKey in not-found error.

Current message mentions token, but the cache is keyed by exchangeKey.

- err = fmt.Errorf("cannot find sending message for token(%v)", r.Token())
+ err = fmt.Errorf("cannot find sending message for exchangeKey(%d)", exchangeKey)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b66b2d0 and 4a27d0a.

📒 Files selected for processing (2)
  • net/blockwise/blockwise.go (19 hunks)
  • udp/server_libcoap_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
udp/server_libcoap_test.go (6)
message/type.go (2)
  • Type (11-11)
  • Confirmable (17-17)
message/option.go (3)
  • MediaType (162-162)
  • TextPlain (166-166)
  • AppOctets (172-172)
message/codes/codes.go (5)
  • Content (29-29)
  • GET (16-16)
  • Changed (28-28)
  • PUT (18-18)
  • POST (17-17)
net/connUDP.go (1)
  • NewListenUDP (233-243)
mux/router.go (1)
  • NewRouter (67-83)
options/commonOptions.go (1)
  • WithMux (120-124)
net/blockwise/blockwise.go (3)
message/pool/message.go (1)
  • Message (26-41)
message/getToken.go (1)
  • Token (9-9)
pkg/cache/cache.go (2)
  • NewElement (32-39)
  • Element (14-18)
🪛 GitHub Actions: Golangci-lint
udp/server_libcoap_test.go

[error] 361-361: golangci-lint: non-wrapping format verb for fmt.Errorf. Use %w to format errors (errorlint).

🪛 GitHub Check: lint
udp/server_libcoap_test.go

[failure] 318-318:
indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)


[failure] 219-219:
indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)


[failure] 114-114:
indent-error-flow: if block ends with a return statement, so drop this else and outdent its block (revive)


[failure] 342-342:
integer-format: fmt.Sprintf can be replaced with faster strconv.FormatUint (perfsprint)


[failure] 334-334:
error-format: fmt.Errorf can be replaced with errors.New (perfsprint)


[failure] 283-283:
shadow: declaration of "err" shadows declaration at line 264 (govet)


[failure] 185-185:
shadow: declaration of "err" shadows declaration at line 165 (govet)


[failure] 20-20:
File is not properly formatted (gofumpt)


[failure] 5-5:
File is not properly formatted (gci)


[failure] 361-361:
non-wrapping format verb for fmt.Errorf. Use %w to format errors (errorlint)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Fuzzing
  • GitHub Check: test (windows-latest)
🔇 Additional comments (3)
net/blockwise/blockwise.go (1)

910-925: LGTM: getExchangeKey matches RFC intent and is simple/robust.

Hashing remoteAddr for empty tokens is reasonable; low collision risk for FNV-1a 64-bit.

Please confirm remoteAddr.String() includes port for your transports (expected “ip:port”). If not, consider including family/zone for uniqueness.

udp/server_libcoap_test.go (2)

3-17: Fix import ordering: run gci and gofumpt locally.

The import block has mixed standard library and third-party imports (e.g., bytes, fmt before math/rand, os/exec, sync, testing). These tools are not available in the sandbox, so you'll need to run them locally:

gci write -s standard -s default -s blank -s dot -s local .
gofumpt -w .

This will group standard library imports separately from third-party imports and apply consistent formatting.


341-343: The review comment is based on an incorrect assumption about libcoap's -T behavior.

libcoap treats the absence of a Token as a valid zero-length token, and coap-client's -T option, when omitted, forces a zero-length token—not a default non-empty token. The current code correctly implements this:

  • When token=true: appends -T with a random Uint64 (non-empty token)
  • When token=false: omits -T entirely, resulting in a zero-length token

The "without-token" tests (e.g., "block-without-token") already exercise the empty-token scenario correctly. No changes are required.

Likely an incorrect or invalid review comment.

Comment on lines +118 to +124
if tt.wantPayload != nil {
if wantPayloadBytes, ok := tt.wantPayload.([]byte); ok {
// add 1 for the extra newline added by coap-client
require.Len(t, receivedBody, len(wantPayloadBytes)+1)
require.Equal(t, wantPayloadBytes, receivedBody[0:len(wantPayloadBytes)])
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make GET payload check effective.

wantPayload is a string here, but the assertion only runs for []byte; compare consistently as bytes.

- if tt.wantPayload != nil {
-   if wantPayloadBytes, ok := tt.wantPayload.([]byte); ok {
-     // add 1 for the extra newline added by coap-client
-     require.Len(t, receivedBody, len(wantPayloadBytes)+1)
-     require.Equal(t, wantPayloadBytes, receivedBody[0:len(wantPayloadBytes)])
-   }
- }
+ if tt.wantPayload != nil {
+   want := []byte(tt.wantPayload.(string))
+   // add 1 for the extra newline added by coap-client
+   require.Len(t, receivedBody, len(want)+1)
+   require.Equal(t, want, receivedBody[:len(want)])
+ }

Or change table to use []byte for all payload expectations.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if tt.wantPayload != nil {
if wantPayloadBytes, ok := tt.wantPayload.([]byte); ok {
// add 1 for the extra newline added by coap-client
require.Len(t, receivedBody, len(wantPayloadBytes)+1)
require.Equal(t, wantPayloadBytes, receivedBody[0:len(wantPayloadBytes)])
}
}
if tt.wantPayload != nil {
want := []byte(tt.wantPayload.(string))
// add 1 for the extra newline added by coap-client
require.Len(t, receivedBody, len(want)+1)
require.Equal(t, want, receivedBody[:len(want)])
}
🤖 Prompt for AI Agents
In udp/server_libcoap_test.go around lines 118 to 124, the GET payload assertion
only runs when tt.wantPayload is a []byte but the table uses a string; make the
assertion work for strings by converting expected string to bytes (or update the
table to use []byte for all payload expectations). Change the conditional to
handle both []byte and string (e.g. if b, use as-is; if s, []byte(s)), then
compare lengths and bytes against receivedBody the same way so the test actually
checks string payloads.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
udp/server_libcoap_test.go (1)

113-119: Fix broken payload assertion in GET test.

The test table defines wantPayload as bigPayload10KiB (a string constant at line 350), but the assertion at line 114 only runs when wantPayload is []byte. This means the payload is never actually verified in GET tests.

Apply this diff to fix the assertion:

  if tt.wantPayload != nil {
-   if wantPayloadBytes, ok := tt.wantPayload.([]byte); ok {
-     // add 1 for the extra newline added by coap-client
-     require.Len(t, receivedBody, len(wantPayloadBytes)+1)
-     require.Equal(t, wantPayloadBytes, receivedBody[0:len(wantPayloadBytes)])
-   }
+   want := []byte(tt.wantPayload.(string))
+   // add 1 for the extra newline added by coap-client
+   require.Len(t, receivedBody, len(want)+1)
+   require.Equal(t, want, receivedBody[:len(want)])
  }
🧹 Nitpick comments (4)
mux/router.go (1)

125-136: LGTM! Explicit returns improve readability.

Converting from bare returns to explicit return statements makes the code more readable and maintainable. This is a good practice, especially in functions with multiple exit points.

dtls/server_test.go (1)

88-138: LGTM! Explicit error returns enhance clarity.

Converting all error paths from bare returns to explicit return statements improves code clarity and makes error propagation more obvious. This pattern is now consistent throughout the function and aligns well with the changes in tcp/server_test.go.

tcp/server_test.go (1)

85-133: LGTM! Consistent with DTLS test improvements.

The explicit return statements follow the same refactoring pattern applied in dtls/server_test.go, making error handling more transparent across the test suite. This consistency is valuable for maintainability.

udp/server_libcoap_test.go (1)

325-326: Remove wasteful call to rand.Uint64().

Line 325 calls rand.Uint64() but discards the result. The next line generates a new random value for the token.

Apply this diff:

  if token {
-   rand.Uint64()
    args = append(args, "-T", strconv.FormatUint(rand.Uint64(), 10))
  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a27d0a and 522b23e.

📒 Files selected for processing (6)
  • .github/workflows/test-for-fork.yml (1 hunks)
  • .github/workflows/test.yml (2 hunks)
  • dtls/server_test.go (3 hunks)
  • mux/router.go (2 hunks)
  • tcp/server_test.go (3 hunks)
  • udp/server_libcoap_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
tcp/server_test.go (2)
examples/dtls/pki/cert_gen.go (1)
  • GenerateCertificate (82-122)
examples/dtls/pki/cert_util.go (2)
  • LoadKeyAndCertificate (67-78)
  • LoadCertPool (81-96)
mux/router.go (1)
mux/router_test.go (4)
  • testRoute (135-158)
  • TestMux (20-121)
  • title (11-18)
  • testRegexp (123-133)
udp/server_libcoap_test.go (6)
message/option.go (3)
  • MediaType (162-162)
  • TextPlain (166-166)
  • AppOctets (172-172)
message/codes/codes.go (5)
  • Content (29-29)
  • GET (16-16)
  • Changed (28-28)
  • PUT (18-18)
  • POST (17-17)
net/connUDP.go (1)
  • NewListenUDP (233-243)
mux/router.go (3)
  • NewRouter (67-83)
  • HandlerFunc (30-30)
  • ResponseWriter (15-20)
message/type.go (2)
  • Confirmable (17-17)
  • Type (11-11)
options/commonOptions.go (1)
  • WithMux (120-124)
dtls/server_test.go (2)
examples/dtls/pki/cert_gen.go (1)
  • GenerateCertificate (82-122)
examples/dtls/pki/cert_util.go (2)
  • LoadKeyAndCertificate (67-78)
  • LoadCertPool (81-96)
🪛 actionlint (1.7.8)
.github/workflows/test.yml

132-132: property "os" is not defined in object type {}

(expression)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Fuzzing
  • GitHub Check: test (macOS-latest)
  • GitHub Check: test (windows-latest)
🔇 Additional comments (4)
.github/workflows/test-for-fork.yml (1)

25-33: LGTM!

The conditional installation of coap-client is correctly implemented for Ubuntu and macOS. Windows tests will gracefully skip when the binary is unavailable, which aligns with the test logic in udp/server_libcoap_test.go.

.github/workflows/test.yml (1)

34-42: LGTM!

The conditional installation of coap-client is correctly implemented for the matrix job.

udp/server_libcoap_test.go (2)

59-64: LGTM!

The use of t.Skip correctly allows CI to pass when coap-client is unavailable. This resolves the previous concern about hard failures.


343-347: LGTM!

The error wrapping now correctly uses %w to preserve the error chain, resolving the previous linting issue.

@jkralik
Copy link
Copy Markdown
Member Author

jkralik commented Oct 28, 2025

Hi @Theoyor . I have merged your changes and also I updated ubuntu/macos to install coap-client. But test with coap-client fails. Pls could you look at it ?

@Theoyor
Copy link
Copy Markdown
Contributor

Theoyor commented Oct 29, 2025

Hi @jkralik thanks for installing and merging. I was quite confused on why it doesn't work anymore. After some testing I found two issues. The first one is what we see more prominent in the error logs of the PR tests. However after fixing that locally, I encountered another, more complicated issue.

Long story short, I developed the testing using an older pre-compiled libcoap coap-client, and it worked. Now I self-compiled the coap-client for another project and the current coap-client sends a different token for each blockwise exchange for the same request. The issue is gone when I tested again with the old one.

Which version of coap-client did you install in the test environment? How should we procceed?

@benpicco
Copy link
Copy Markdown

CoAP observe requires a token

How do you arrive at that conclusion? I can't find that in the section you cite.

The token should be only the concern of the client, the server just echos it back without processing it.

So the client sending a different token for each block request shouldn't be an issue for the server.

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.

4 participants