Skip to content

Allow registering multiple ContentCodecs per media type with optional scheme support#1484

Open
Pranav-0440 wants to merge 4 commits intoeclipse-thingweb:masterfrom
Pranav-0440:fix/allow-different-codecs-the-same-contentType
Open

Allow registering multiple ContentCodecs per media type with optional scheme support#1484
Pranav-0440 wants to merge 4 commits intoeclipse-thingweb:masterfrom
Pranav-0440:fix/allow-different-codecs-the-same-contentType

Conversation

@Pranav-0440
Copy link

Closes #1409

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>
Copilot AI review requested due to automatic review settings February 15, 2026 18:41
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 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 ContentSerdes codec registry to map mediaType -> (scheme -> codec) and adds an optional scheme parameter to addCodec.
  • 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() assumes thing.events[affordanceKey] exists. For requests like /events (missing key) or /events/<unknown>, event will be undefined and event.forms / event.uriVariables will throw. Add an explicit not-found check (and validate affordanceKey) 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 unknown affordanceKey. If thing.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() assumes thing.actions[affordanceKey] exists. For requests like /actions (missing key) or /actions/<unknown>, action will be undefined and action.forms / action.uriVariables will throw. Please restore a not-found guard (and also validate affordanceKey before 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.

Comment on lines +621 to +623
if (
content.type !== ContentSerdes.DEFAULT &&
content.type !== "application/json"
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

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).

Suggested change
if (
content.type !== ContentSerdes.DEFAULT &&
content.type !== "application/json"
const mediaType = ContentSerdes.getMediaType(content.type);
if (
mediaType !== ContentSerdes.DEFAULT &&
mediaType !== "application/json"

Copilot uses AI. Check for mistakes.
Comment on lines +110 to 135
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);
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +110 to 135
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);
}
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
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 {
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
public addCodec(codec: ContentCodec, offered = false, scheme?: string) : void {
public addCodec(codec: ContentCodec, offered = false, scheme?: string): void {

Copilot uses AI. Check for mistakes.
Comment on lines 147 to 152
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);

Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
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.

Allow different Codecs for the same contentType

1 participant