Allow registering multiple ContentCodecs per media type with optional scheme support#1484
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>
There was a problem hiding this comment.
Pull request overview
This PR aims to extend ContentSerdes to support registering multiple ContentCodecs for the same media type (with an additional “scheme” dimension), and includes several CoAP binding cleanups plus DTLS/PSK documentation for CoAPs.
Changes:
- Refactors
ContentSerdescodec registry to mapmediaType -> (scheme -> codec)and adds an optionalschemeparameter toaddCodec. - Updates CoAP binding code paths (content-type handling, request routing, internal refactors) and adds a package-local ESLint config.
- Documents PSK usage for
coaps://in the CoAP binding README.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/content-serdes.ts | Refactors codec storage to support multiple codecs per media type and introduces optional scheme registration. |
| packages/binding-coap/src/mdns-introducer.ts | Simplifies iteration over network interfaces. |
| packages/binding-coap/src/coaps-client.ts | Tightens undefined checks and minor cleanup in security and content handling. |
| packages/binding-coap/src/coap-server.ts | Refactors routing and multiple-properties read behavior; adjusts type handling and content-type defaults. |
| packages/binding-coap/src/coap-client.ts | Makes Content-Format parsing more defensive (non-string headers). |
| packages/binding-coap/eslint.config.mjs | Adds binding-local ESLint override for no-unnecessary-condition. |
| packages/binding-coap/README.md | Adds documentation for DTLS/PSK usage with coaps://. |
Comments suppressed due to low confidence (3)
packages/binding-coap/src/coap-server.ts:860
handleEventRequest()assumesthing.events[affordanceKey]exists. For requests like/events(missing key) or/events/<unknown>,eventwill be undefined andevent.forms/event.uriVariableswill throw. Add an explicit not-found check (and validateaffordanceKey) before using the event.
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
);
packages/binding-coap/src/coap-server.ts:563
handlePropertyRequest()no longer handles an unknownaffordanceKey. Ifthing.properties[affordanceKey]is undefined, subsequent accesses (property.forms,property.readOnly, etc.) will throw and likely crash the request handler instead of returning 4.04. Add an explicit not-found check before switching on the method.
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) {
packages/binding-coap/src/coap-server.ts:788
handleActionRequest()assumesthing.actions[affordanceKey]exists. For requests like/actions(missing key) or/actions/<unknown>,actionwill be undefined andaction.forms/action.uriVariableswill throw. Please restore a not-found guard (and also validateaffordanceKeybefore indexing).
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
);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if ( | ||
| content.type !== ContentSerdes.DEFAULT && | ||
| content.type !== "application/json" |
There was a problem hiding this comment.
In handleReadMultipleProperties(), the JSON filter is too strict: it only accepts content.type exactly equal to application/json / ContentSerdes.DEFAULT. JSON content types with parameters (e.g. application/json; charset=utf-8) will be skipped even though they are JSON. Consider comparing ContentSerdes.getMediaType(content.type) against application/json (or just attempt JSON parsing inside the try/catch without the exact-type guard).
| if ( | |
| content.type !== ContentSerdes.DEFAULT && | |
| content.type !== "application/json" | |
| const mediaType = ContentSerdes.getMediaType(content.type); | |
| if ( | |
| mediaType !== ContentSerdes.DEFAULT && | |
| mediaType !== "application/json" |
| public addCodec(codec: ContentCodec, offered = false, scheme?: string) : void { | ||
| const mediaType = codec.getMediaType(); | ||
|
|
||
| if (!this.codecs.has(mediaType)) { | ||
| this.codecs.set(mediaType, new Map()); | ||
| } | ||
|
|
||
| const schemeMap = this.codecs.get(mediaType)!; | ||
|
|
||
| // store codec under specific scheme (or undefined for generic) | ||
| schemeMap.set(scheme, codec); | ||
|
|
||
| if (offered) { | ||
| this.offered.add(mediaType); | ||
| } | ||
| } | ||
| private getCodec(mediaType: string, scheme?: string): ContentCodec | undefined { | ||
| const schemeMap = this.codecs.get(mediaType); | ||
| if (!schemeMap) return undefined; | ||
|
|
||
| if (scheme !== undefined && schemeMap.has(scheme)) { | ||
| return schemeMap.get(scheme); | ||
| } | ||
|
|
||
| return schemeMap.get(undefined); | ||
| } |
There was a problem hiding this comment.
There is existing automated test coverage for ContentSerdes (see packages/core/test/content-serdes-test.ts), but this change introduces new behavior (multiple codecs per media type and scheme handling) without adding/adjusting tests. Please add tests that register multiple codecs for the same media type (generic + scheme-specific) and verify the correct codec is selected/fallback behavior is correct.
| public addCodec(codec: ContentCodec, offered = false, scheme?: string) : void { | ||
| const mediaType = codec.getMediaType(); | ||
|
|
||
| if (!this.codecs.has(mediaType)) { | ||
| this.codecs.set(mediaType, new Map()); | ||
| } | ||
|
|
||
| const schemeMap = this.codecs.get(mediaType)!; | ||
|
|
||
| // store codec under specific scheme (or undefined for generic) | ||
| schemeMap.set(scheme, codec); | ||
|
|
||
| if (offered) { | ||
| this.offered.add(mediaType); | ||
| } | ||
| } | ||
| private getCodec(mediaType: string, scheme?: string): ContentCodec | undefined { | ||
| const schemeMap = this.codecs.get(mediaType); | ||
| if (!schemeMap) return undefined; | ||
|
|
||
| if (scheme !== undefined && schemeMap.has(scheme)) { | ||
| return schemeMap.get(scheme); | ||
| } | ||
|
|
||
| return schemeMap.get(undefined); | ||
| } |
There was a problem hiding this comment.
The new scheme parameter in addCodec() is currently ineffective: getCodec() is private and every caller (isSupported, contentToValue, valueToContent) calls it without passing a scheme, so scheme-specific codecs can never be selected (and isSupported() will return false for a media type that only has a scheme-specific codec). To make this feature usable, expose scheme-aware lookup and thread the scheme through the serialization/deserialization entry points (or otherwise define how the correct codec is chosen).
| 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 style: method signature spacing is inconsistent with the rest of the file (scheme?: string) : void). Please align to the existing ): void formatting to keep lint/formatting consistent.
| public addCodec(codec: ContentCodec, offered = false, scheme?: string) : void { | |
| public addCodec(codec: ContentCodec, offered = false, scheme?: string): void { |
| public async expose(thing: ExposedThing, tdTemplate?: WoT.ExposedThingInit): Promise<void> { | ||
| const port = this.getPort(); | ||
| const urlPath = this.createThingUrlPath(thing); | ||
|
|
||
| if (port === -1) { | ||
| warn("CoapServer is assigned an invalid port, aborting expose process."); | ||
| return; | ||
| } | ||
|
|
||
| this.fillInBindingData(thing, port, urlPath); | ||
|
|
There was a problem hiding this comment.
expose() no longer guards against getPort() returning -1. Servient.addServer() can call server.expose() before the server has started, which would now generate forms/URLs containing port -1. Consider restoring the early return (or otherwise ensuring expose() only runs after start() has bound the socket).
Closes #1409