Feat/multiple codecs same mediatype#1485
Feat/multiple codecs same mediatype#1485Pranav-0440 wants to merge 6 commits intoeclipse-thingweb:masterfrom
Conversation
Add example and explanation for configuring PSK security scheme in CoapsClient. Closes eclipse-thingweb#954 Signed-off-by: Pranav Ghorpade <pranavghorpade61@gmail.com>
Add example and explanation for configuring PSK security scheme in CoapsClient. Closes eclipse-thingweb#954 Signed-off-by: Pranav Ghorpade <pranavghorpade61@gmail.com>
…ssary-condition rule Signed-off-by: Pranav Ghorpade <pranavghorpade61@gmail.com>
…ith optional scheme support Signed-off-by: Pranav Ghorpade <pranavghorpade61@gmail.com>
…ith optional scheme support Signed-off-by: Pranav Ghorpade <pranavghorpade61@gmail.com>
There was a problem hiding this comment.
Pull request overview
This PR aims to address #1409 by enabling multiple ContentCodecs to be registered for the same media type (intended to disambiguate by protocol/scheme), alongside several CoAP binding refactors and documentation/lint additions.
Changes:
- Extend
ContentSerdesinternal codec registry to allow multiple codecs per media type (keyed by optional scheme). - Refactor CoAP binding code for stricter undefined handling and updated request/response content-type processing.
- Add binding-specific ESLint override and expand CoAP README with CoAPs/DTLS PSK usage documentation.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/content-serdes.ts | Changes codec registry shape to support multiple codecs per media type (scheme-keyed). |
| packages/binding-coap/src/mdns-introducer.ts | Simplifies iteration over networkInterfaces() results. |
| packages/binding-coap/src/coaps-client.ts | Large reformat + minor logic tweaks around security/URL parsing. |
| packages/binding-coap/src/coap-server.ts | Refactors request routing, affordance form handling, and multi-property read logic. |
| packages/binding-coap/src/coap-client.ts | Tightens Content-Format header handling to avoid non-string cases. |
| packages/binding-coap/eslint.config.mjs | Adds package-level ESLint rule override for unnecessary conditions. |
| packages/binding-coap/README.md | Documents CoAPs (DTLS) PSK usage and provides examples. |
Comments suppressed due to low confidence (4)
packages/binding-coap/src/coap-server.ts:155
expose()no longer guards againstgetPort()returning-1(which happens when the server socket is not yet available). With the guard removed, this can generate invalid forms/URLs (port-1) and register MDNS entries with an invalid port. Consider restoring the early return or throwing a clear error whenport === -1.
public async expose(thing: ExposedThing, tdTemplate?: WoT.ExposedThingInit): Promise<void> {
const port = this.getPort();
const urlPath = this.createThingUrlPath(thing);
this.fillInBindingData(thing, port, urlPath);
debug(`CoapServer on port ${port} exposes '${thing.title}' as unique '/${urlPath}'`);
this.setUpIntroductionMethods(thing, urlPath, port);
packages/binding-coap/src/coap-server.ts:569
handlePropertyRequest()no longer handles the case wherething.properties[affordanceKey]is undefined. The method later dereferencesproperty.readOnly,property.forms, etc., which will throw and turn a client 404 into a 5.00. Please restore a not-found path (e.g., send 4.04 or route tohandlePropertiesRequestas before) when the property doesn’t exist.
private async handlePropertyRequest(
thing: ExposedThing,
affordanceKey: string,
req: IncomingMessage,
res: OutgoingMessage,
contentType: string
) {
const property = thing.properties[affordanceKey];
switch (req.method) {
case "GET":
if (req.headers.Observe == null) {
this.handleReadProperty(property, req, contentType, thing, res, affordanceKey);
} else {
this.handleObserveProperty(property, req, contentType, thing, res, affordanceKey);
}
break;
case "PUT":
if (property.readOnly === true) {
this.sendResponse(res, "4.00", "Property readOnly");
return;
}
this.handleWriteProperty(property, req, contentType, thing, res, affordanceKey);
break;
packages/binding-coap/src/coap-server.ts:788
handleActionRequest()assumesthing.actions[affordanceKey]exists, but there’s no longer a not-found check before dereferencingaction.forms/action.uriVariables. This will throw for unknown actions and return 5.00 instead of 4.04. Add an explicit not-found response whenactionis undefined.
private async handleActionRequest(
thing: ExposedThing,
affordanceKey: string,
req: IncomingMessage,
res: OutgoingMessage,
contentType: string
) {
const action = thing.actions[affordanceKey];
if (req.method !== "POST") {
this.sendMethodNotAllowedResponse(res);
return;
}
const interactionOptions = this.createInteractionOptions(
action.forms,
thing,
req,
contentType,
action.uriVariables
);
packages/binding-coap/src/coap-server.ts:861
handleEventRequest()assumesthing.events[affordanceKey]exists, but there’s no longer a not-found check before dereferencingevent.forms/event.uriVariables. This will throw for unknown events and return 5.00 instead of 4.04. Add an explicit not-found response wheneventis undefined.
private async handleEventRequest(
thing: ExposedThing,
affordanceKey: string,
req: IncomingMessage,
res: OutgoingMessage,
contentType: string
) {
const event = thing.events[affordanceKey];
if (req.method !== "GET") {
this.sendMethodNotAllowedResponse(res);
return;
}
const observe = req.headers.Observe as number | undefined;
if (observe === undefined) {
debug(
`CoapServer on port ${this.getPort()} rejects '${affordanceKey}' event subscription from ${Helpers.toUriLiteral(
req.rsinfo.address
)}:${req.rsinfo.port}`
);
this.sendResponse(res, "4.00", "No Observe Option");
return;
}
if (observe === 0) {
this.avoidDuplicatedObserveRegistration(res); // TODO: Get rid of this workaround
const interactionOptions = this.createInteractionOptions(
event.forms,
thing,
req,
contentType,
event.uriVariables
);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -122,7 +144,7 @@ export class ContentSerdes { | |||
|
|
|||
| public isSupported(contentType: string): boolean { | |||
| const mt = ContentSerdes.getMediaType(contentType); | |||
| return this.codecs.has(mt); | |||
| return this.getCodec(mt) !== undefined; | |||
| } | |||
|
|
|||
| public contentToValue(content: ReadContent, schema: DataSchema): DataSchemaValue | undefined { | |||
| @@ -141,20 +163,15 @@ export class ContentSerdes { | |||
| const par = ContentSerdes.getMediaTypeParameters(content.type); | |||
|
|
|||
| // choose codec based on mediaType | |||
| if (this.codecs.has(mt)) { | |||
| debug(`ContentSerdes deserializing from ${content.type}`); | |||
|
|
|||
| const codec = this.codecs.get(mt); | |||
|
|
|||
| // use codec to deserialize | |||
| // this.codecs.has(mt) is true | |||
| const res = codec!.bytesToValue(content.body, schema, par); | |||
| const codec = this.getCodec(mt); | |||
|
|
|||
| return res; | |||
| } else { | |||
| warn(`ContentSerdes passthrough due to unsupported media type '${mt}'`); | |||
| return content.body.toString(); | |||
| } | |||
| if (codec) { | |||
| debug(`ContentSerdes deserializing from ${content.type}`); | |||
| return codec.bytesToValue(content.body, schema, par); | |||
| } else { | |||
| warn(`ContentSerdes passthrough due to unsupported media type '${mt}'`); | |||
| return content.body.toString(); | |||
| } | |||
| } | |||
There was a problem hiding this comment.
The new scheme support is currently not usable: contentToValue(), valueToContent(), and isSupported() call getCodec(mt) without a scheme, so codecs registered only for a specific scheme will never be selected (and isSupported() will return false). This also means a scheme-specific codec marked as offered can end up advertised even though serialization/deserialization can’t use it. Consider threading the URI scheme/protocol (or another selector) through the public APIs, or adjusting codec resolution semantics so scheme-specific codecs can be discovered/used when no generic codec is present.
|
|
||
| const content = ContentSerdes.get().valueToContent(recordResponse, undefined, contentType); | ||
| this.streamContentResponse(res, content); | ||
| const responseContent = ContentSerdes.get().valueToContent(recordResponse,undefined,contentType); |
There was a problem hiding this comment.
Formatting nit: valueToContent(recordResponse,undefined,contentType) is missing spaces after commas, which is inconsistent with surrounding calls and may fail formatting/lint checks. Please format consistently.
| const responseContent = ContentSerdes.get().valueToContent(recordResponse,undefined,contentType); | |
| const responseContent = ContentSerdes.get().valueToContent(recordResponse, undefined, contentType); |
| complete?: () => void | ||
| ): Promise<Subscription> { | ||
| return new Promise<Subscription>((resolve, reject) => { | ||
| const requestUri = new URL(form.href.replace(/$coaps/, "https")); |
There was a problem hiding this comment.
form.href.replace(/$coaps/, "https") uses a regex that will never match ($ anchors end-of-string, so it can’t be followed by coaps). Either remove the replacement (Node’s URL can parse coaps:// already) or correct it to match the scheme prefix if that’s still needed.
| const requestUri = new URL(form.href.replace(/$coaps/, "https")); | |
| const requestUri = new URL(form.href); |
| content?: Content | ||
| ): Promise<CoapResponse> { | ||
| // url only works with http* | ||
| const requestUri = new URL(form.href.replace(/$coaps/, "https")); |
There was a problem hiding this comment.
Same issue as above: new URL(form.href.replace(/$coaps/, "https")) uses a regex that never matches, making the replacement a no-op and confusing the intent. Consider removing it or fixing the regex to target the scheme prefix.
| const requestUri = new URL(form.href.replace(/$coaps/, "https")); | |
| const requestUri = new URL(form.href.replace(/^coaps:/, "https:")); |
| public addCodec(codec: ContentCodec, offered = false): void { | ||
| ContentSerdes.get().codecs.set(codec.getMediaType(), codec); | ||
| if (offered) ContentSerdes.get().offered.add(codec.getMediaType()); | ||
| public addCodec(codec: ContentCodec, offered = false, scheme?: string) : void { |
There was a problem hiding this comment.
Minor formatting: public addCodec(..., scheme?: string) : void has an extra space before the return-type colon; the rest of this file uses ): void. Keeping formatting consistent helps avoid churn in future diffs.
| public addCodec(codec: ContentCodec, offered = false, scheme?: string) : void { | |
| public addCodec(codec: ContentCodec, offered = false, scheme?: string): void { |
| const thing = await WoT.consume(td); | ||
|
|
||
| // configure PSK security | ||
| thing.setSecurity(td.securityDefinitions, { | ||
| identity: "Client_identity", | ||
| psk: "secretPSK", | ||
| }); | ||
|
|
There was a problem hiding this comment.
The PSK client example uses thing.setSecurity(td.securityDefinitions, { ... }), but in this codebase protocol clients receive setSecurity(Array<SecurityScheme>, credentials) and the usual way to provide credentials is via Servient.addCredentials() (keyed by the Thing id), as shown in other binding READMEs. As written, this example is likely not executable. Please align the example with the supported API (e.g., add credentials to the Servient after fetching the TD, and let ConsumedThing apply them).
| const thing = await WoT.consume(td); | |
| // configure PSK security | |
| thing.setSecurity(td.securityDefinitions, { | |
| identity: "Client_identity", | |
| psk: "secretPSK", | |
| }); | |
| // configure PSK security credentials for this Thing | |
| servient.addCredentials({ | |
| [td.id]: { | |
| identity: "Client_identity", | |
| psk: "secretPSK", | |
| }, | |
| }); | |
| const thing = await WoT.consume(td); |
Signed-off-by: Pranav Ghorpade <pranavghorpade61@gmail.com>
Closes #1409