Skip to content

Feat/multiple codecs same mediatype#1485

Open
Pranav-0440 wants to merge 6 commits intoeclipse-thingweb:masterfrom
Pranav-0440:feat/multiple-codecs-same-mediatype
Open

Feat/multiple codecs same mediatype#1485
Pranav-0440 wants to merge 6 commits intoeclipse-thingweb:masterfrom
Pranav-0440:feat/multiple-codecs-same-mediatype

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>
…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:47
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 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 ContentSerdes internal 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 against getPort() 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 when port === -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 where thing.properties[affordanceKey] is undefined. The method later dereferences property.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 to handlePropertiesRequest as 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() assumes thing.actions[affordanceKey] exists, but there’s no longer a not-found check before dereferencing action.forms / action.uriVariables. This will throw for unknown actions and return 5.00 instead of 4.04. Add an explicit not-found response when action is 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() assumes thing.events[affordanceKey] exists, but there’s no longer a not-found check before dereferencing event.forms / event.uriVariables. This will throw for unknown events and return 5.00 instead of 4.04. Add an explicit not-found response when event is 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.

Comment on lines 110 to 175
@@ -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();
}
}
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 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.

Copilot uses AI. Check for mistakes.

const content = ContentSerdes.get().valueToContent(recordResponse, undefined, contentType);
this.streamContentResponse(res, content);
const responseContent = ContentSerdes.get().valueToContent(recordResponse,undefined,contentType);
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.

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.

Suggested change
const responseContent = ContentSerdes.get().valueToContent(recordResponse,undefined,contentType);
const responseContent = ContentSerdes.get().valueToContent(recordResponse, undefined, contentType);

Copilot uses AI. Check for mistakes.
complete?: () => void
): Promise<Subscription> {
return new Promise<Subscription>((resolve, reject) => {
const requestUri = new URL(form.href.replace(/$coaps/, "https"));
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.

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.

Suggested change
const requestUri = new URL(form.href.replace(/$coaps/, "https"));
const requestUri = new URL(form.href);

Copilot uses AI. Check for mistakes.
content?: Content
): Promise<CoapResponse> {
// url only works with http*
const requestUri = new URL(form.href.replace(/$coaps/, "https"));
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.

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.

Suggested change
const requestUri = new URL(form.href.replace(/$coaps/, "https"));
const requestUri = new URL(form.href.replace(/^coaps:/, "https:"));

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

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 +99 to +106
const thing = await WoT.consume(td);

// configure PSK security
thing.setSecurity(td.securityDefinitions, {
identity: "Client_identity",
psk: "secretPSK",
});

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

Suggested change
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);

Copilot uses AI. Check for mistakes.
Signed-off-by: Pranav Ghorpade <pranavghorpade61@gmail.com>
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