Skip to content

Support ID-based CoAP paths for exposed Things#1490

Open
Pranav-0440 wants to merge 2 commits intoeclipse-thingweb:masterfrom
Pranav-0440:feat/coap-id-based-paths-1458
Open

Support ID-based CoAP paths for exposed Things#1490
Pranav-0440 wants to merge 2 commits intoeclipse-thingweb:masterfrom
Pranav-0440:feat/coap-id-based-paths-1458

Conversation

@Pranav-0440
Copy link

This PR implements ID-based path support for the CoAP binding, similar to the HTTP implementation introduced in #1464.

Changes:

  • Added devFriendlyUri configuration option (default: true)
  • Implemented dual-path registration (slugified title + id-based path)
  • Ensured proper cleanup of all aliases in destroy()
  • Added tests verifying reachability via both title and id paths

This maintains backward compatibility while enabling stable, production-safe URIs for CoAP-exposed Things.
Fixes - #1458

…eb#1458)

Signed-off-by: Pranav-0440 <pranavghorpade61@gmail.com>
Copilot AI review requested due to automatic review settings February 17, 2026 18:40
@Pranav-0440 Pranav-0440 requested a review from JKRhb as a code owner February 17, 2026 18:40
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements ID-based path support for the CoAP binding, mirroring the functionality introduced in PR #1464 for the HTTP binding. It addresses issue #1458 by enabling stable, production-safe URIs based on Thing IDs while maintaining backward compatibility through title-based paths.

Changes:

  • Added devFriendlyUri configuration option (defaults to true for backward compatibility)
  • Implemented dual-path registration allowing Things to be accessed via both slugified title and ID-based paths
  • Updated destroy() method to properly clean up all registered path aliases for a Thing
  • Added test validating reachability through both path types

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
packages/binding-coap/src/coap.ts Added devFriendlyUri configuration option to CoapServerConfig interface
packages/binding-coap/src/coap-server.ts Refactored path creation to support dual registration, updated expose() and destroy() methods to handle multiple paths per Thing
packages/binding-coap/test/coap-server-test.ts Added test verifying Things are reachable via both title-based and ID-based paths

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

let urlPath = slugify(thing.title, { lower: true });
private createThingUrlPaths(thing: ExposedThing): string[] {
const paths: string[] = [];
// Title-based path
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

Consider adding a comment explaining why the title-based path is created even when devFriendlyUri is false if no ID is provided, similar to the explanatory comment in the HTTP binding at http-server.ts:274. This helps future maintainers understand the fallback behavior for Things without IDs.

Suggested change
// Title-based path
// Title-based path
// Even when devFriendlyUri is false we still create a title-based path if the Thing has no ID.
// This mirrors the HTTP binding behavior and ensures that Things without IDs can still be exposed
// under a stable, discoverable path derived from their title.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

I think this comment could rather be used as the basis for a doc comment for the function. Generally speaking, though, I don't think the mirroring of the HTTP binding needs to be mentioned that often.

await coapClient.stop();
await coapServer.stop();
}

Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The test validates dual-path registration but doesn't cover the case where devFriendlyUri is set to false. Consider adding a test case that verifies when devFriendlyUri is false and a Thing has an ID, only the ID-based path is registered (not the title-based path).

Suggested change
@test async "should expose Thing with id and title and be reachable only by id when devFriendlyUri is false"() {
const coapServer = new CoapServer({ port: PORT, devFriendlyUri: false });
await coapServer.start(new Servient());
const testThing = new ExposedThing(new Servient(), {
title: "TestThing",
id: "urn:dev:wot:test-thing-1234",
properties: {
test: {
type: "string",
},
},
});
testThing.setPropertyReadHandler("test", async () => "OK");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
testThing.properties.test.forms = [];
await coapServer.expose(testThing);
const uriByTitle = `coap://localhost:${coapServer.getPort()}/testthing/properties/test`;
const uriById = `coap://localhost:${coapServer.getPort()}/urn:dev:wot:test-thing-1234/properties/test`;
const coapClient = new CoapClient(coapServer);
// ID-based path should be reachable
const respById = await coapClient.readResource(new Form(uriById));
expect((await respById.toBuffer()).toString()).to.equal('"OK"');
// Title-based path should not be registered when devFriendlyUri is false
const respByTitle = await new Promise<IncomingMessage>((resolve) => {
const req = request(uriByTitle);
req.on("response", (res) => resolve(res));
req.end();
});
expect(respByTitle.code).to.equal("4.04");
await coapClient.stop();
await coapServer.stop();
}

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

Seems like a valid addition at first glance.

@@ -30,6 +30,7 @@ export * from "./coaps-client";
export interface CoapServerConfig {
port?: number;
address?: string;
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

Consider adding JSDoc documentation for the devFriendlyUri configuration option to explain its purpose and behavior. Similar to the HTTP binding, this option controls whether title-based paths are generated (true by default for backward compatibility). When false and a Thing has an ID, only ID-based paths are created.

Suggested change
address?: string;
address?: string;
/**
* Controls whether title-based CoAP resource paths are generated.
*
* Similar to the HTTP binding, this option is `true` by default for backward
* compatibility, so that Things are exposed using paths derived from their
* titles.
*
* When set to `false` and a Thing has an ID, only ID-based paths are created
* for that Thing.
*/

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

The reference to the HTTP binding could be removed here.

paths.push(urlPath);
}
// ID-based path
if (typeof thing.id === "string" && thing.id.length > 0) {
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The ID-based path registration doesn't check for collisions with existing paths. If a Thing's ID matches another Thing's title-based path, it will silently overwrite the existing registration. Consider adding collision detection for ID paths or at least logging a warning when an ID path overwrites an existing path, similar to how title-based paths handle collisions with the numeric suffix pattern.

Suggested change
if (typeof thing.id === "string" && thing.id.length > 0) {
if (typeof thing.id === "string" && thing.id.length > 0) {
if (this.things.has(thing.id)) {
warn(
`CoapServer path collision detected: Thing with id '${thing.id}' will overwrite existing Thing registration at path '/${thing.id}'.`
);
}

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

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

Oh, that is actually another interesting observation.

@codecov
Copy link

codecov bot commented Feb 18, 2026

Codecov Report

❌ Patch coverage is 48.78049% with 21 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.60%. Comparing base (2fd12dd) to head (96489f8).
⚠️ Report is 22 commits behind head on master.

Files with missing lines Patch % Lines
packages/binding-coap/src/coap-server.ts 47.50% 21 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1490      +/-   ##
==========================================
- Coverage   77.99%   77.60%   -0.39%     
==========================================
  Files          86       86              
  Lines       15904    15917      +13     
  Branches     1509     1511       +2     
==========================================
- Hits        12404    12353      -51     
- Misses       3479     3541      +62     
- Partials       21       23       +2     

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Member

@JKRhb JKRhb left a comment

Choose a reason for hiding this comment

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

Thank you, @Pranav-0440, see a couple of comments below:

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const paths = this.createThingUrlPaths(thing);
const urlPaths = this.createThingUrlPaths(thing);

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
for (const urlPath of paths) {
for (const urlPath of urlPaths) {

let urlPath = slugify(thing.title, { lower: true });
private createThingUrlPaths(thing: ExposedThing): string[] {
const paths: string[] = [];
// Title-based path
Copy link
Member

Choose a reason for hiding this comment

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

I think this comment could rather be used as the basis for a doc comment for the function. Generally speaking, though, I don't think the mirroring of the HTTP binding needs to be mentioned that often.

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const paths: string[] = [];
const urlPaths: string[] = [];

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
paths.push(urlPath);
urlPaths.push(urlPath);

Comment on lines +170 to +177
if (this.things.has(urlPath)) {
let uniqueUrlPath;
let nameClashCnt = 2;
do {
uniqueUrlPath = urlPath + "_" + nameClashCnt++;
} while (this.things.has(uniqueUrlPath));
urlPath = uniqueUrlPath;
}
Copy link
Member

Choose a reason for hiding this comment

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

Hmm. I was wondering whether we could end up in the following situation here:

  • Starting point: two (partial) TDs have the same title, but only the second one has an ID
  • For the second TD, a nameclash regarding the title is detected because the first TD did not have an ID and therefore an identical URL path
  • The title of the second TD is appended with a _2
  • The urlPath of the second ID is further expanded by the ID

This is certainly an edge case (I am not actually not certain at the moment whether it is possible to have a missing ID at this point or whether that one is going to be set by the Servient), but I still wanted to mention it here.

paths.push(urlPath);
}
// ID-based path
if (typeof thing.id === "string" && thing.id.length > 0) {
Copy link
Member

Choose a reason for hiding this comment

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

Oh, that is actually another interesting observation.

@@ -30,6 +30,7 @@ export * from "./coaps-client";
export interface CoapServerConfig {
port?: number;
address?: string;
Copy link
Member

Choose a reason for hiding this comment

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

The reference to the HTTP binding could be removed here.

await coapClient.stop();
await coapServer.stop();
}

Copy link
Member

Choose a reason for hiding this comment

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

Seems like a valid addition at first glance.

Signed-off-by: Pranav-0440 <pranavghorpade61@gmail.com>
@Pranav-0440
Copy link
Author

Thanks @JKRhb for the review!

  • Renamed internal variables for consistency (urlPaths)
  • Moved the fallback explanation into a proper doc comment
  • Added collision warning for ID-based paths
  • Added a test covering devFriendlyUri = false

All tests pass locally. Please let me know if further refinements are needed.

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.

2 participants

Comments