fix(plugins): add base64 handling for []byte and remove raw=true (#5121)

* fix(plugins): add base64 handling for []byte and remove raw=true

Go's json.Marshal automatically base64-encodes []byte fields, but Rust's
serde_json serializes Vec<u8> as a JSON array and Python's json.dumps
raises TypeError on bytes. This fixes both directions of plugin
communication by adding proper base64 encoding/decoding in generated
client code.

For Rust templates (client and capability): adds a base64_bytes serde
helper module with #[serde(with = "base64_bytes")] on all Vec<u8> fields,
and adds base64 as a dependency. For Python templates: wraps bytes params
with base64.b64encode() and responses with base64.b64decode().

Also removes the raw=true binary framing protocol from all templates,
the parser, and the Method type. The raw mechanism added complexity that
is no longer needed once []byte works properly over JSON.

* fix(plugins): update production code and tests for base64 migration

Remove raw=true annotation from SubsonicAPI.CallRaw, delete all raw
test fixtures, remove raw-related test cases from parser, generator, and
integration tests, and add new test cases validating base64 handling
for Rust and Python templates.

* fix(plugins): update golden files and regenerate production code

Update golden test fixtures for codec and comprehensive services to
include base64 handling for []byte fields. Regenerate all production
PDK code (Go, Rust, Python) and host wrappers to use standard JSON
with base64-encoded byte fields instead of binary framing protocol.

* refactor: remove base64 helper duplication from rust template

Signed-off-by: Deluan <deluan@navidrome.org>

* fix(plugins): add base64 dependency to capabilities' Cargo.toml

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2026-02-27 19:00:19 -05:00
committed by GitHub
parent 582d1b3cd9
commit bd8032b327
36 changed files with 460 additions and 854 deletions
+3 -2
View File
@@ -12,6 +12,7 @@ from typing import Any
import extism
import json
import base64
class HostFunctionError(Exception):
@@ -337,7 +338,7 @@ Returns an error if the operation fails.
"""
request = {
"key": key,
"value": value,
"value": base64.b64encode(value).decode("ascii"),
"ttlSeconds": ttl_seconds,
}
request_bytes = json.dumps(request).encode("utf-8")
@@ -382,7 +383,7 @@ or the stored value is not a byte slice, exists will be false.
raise HostFunctionError(response["error"])
return CacheGetBytesResult(
value=response.get("value", b""),
value=base64.b64decode(response.get("value", "")),
exists=response.get("exists", False),
)
+1
View File
@@ -12,6 +12,7 @@ from typing import Any
import extism
import json
import base64
class HostFunctionError(Exception):
+3 -2
View File
@@ -12,6 +12,7 @@ from typing import Any
import extism
import json
import base64
class HostFunctionError(Exception):
@@ -80,7 +81,7 @@ Returns an error if the storage limit would be exceeded or the operation fails.
"""
request = {
"key": key,
"value": value,
"value": base64.b64encode(value).decode("ascii"),
}
request_bytes = json.dumps(request).encode("utf-8")
request_mem = extism.memory.alloc(request_bytes)
@@ -123,7 +124,7 @@ Returns the value and whether the key exists.
raise HostFunctionError(response["error"])
return KVStoreGetResult(
value=response.get("value", b""),
value=base64.b64decode(response.get("value", "")),
exists=response.get("exists", False),
)
+21 -21
View File
@@ -8,11 +8,11 @@
# main __init__.py file. Copy the needed functions from this file into your plugin.
from dataclasses import dataclass
from typing import Any, Tuple
from typing import Any
import extism
import json
import struct
import base64
class HostFunctionError(Exception):
@@ -32,6 +32,13 @@ def _subsonicapi_callraw(offset: int) -> int:
...
@dataclass
class SubsonicAPICallRawResult:
"""Result type for subsonicapi_call_raw."""
content_type: str
data: bytes
def subsonicapi_call(uri: str) -> str:
"""Call executes a Subsonic API request and returns the JSON response.
@@ -62,16 +69,16 @@ e.g., "getAlbumList2?type=random&size=10". The response is returned as raw JSON.
return response.get("responseJson", "")
def subsonicapi_call_raw(uri: str) -> Tuple[str, bytes]:
def subsonicapi_call_raw(uri: str) -> SubsonicAPICallRawResult:
"""CallRaw executes a Subsonic API request and returns the raw binary response.
Optimized for binary endpoints like getCoverArt and stream that return
non-JSON data. The response is returned as raw bytes without JSON encoding overhead.
Designed for binary endpoints like getCoverArt and stream that return
non-JSON data. The data is base64-encoded over JSON on the wire.
Args:
uri: str parameter.
Returns:
Tuple of (content_type, data) with the raw binary response.
SubsonicAPICallRawResult containing content_type, data,.
Raises:
HostFunctionError: If the host function returns an error.
@@ -83,19 +90,12 @@ non-JSON data. The response is returned as raw bytes without JSON encoding overh
request_mem = extism.memory.alloc(request_bytes)
response_offset = _subsonicapi_callraw(request_mem.offset)
response_mem = extism.memory.find(response_offset)
response_bytes = response_mem.bytes()
response = json.loads(extism.memory.string(response_mem))
if len(response_bytes) == 0:
raise HostFunctionError("empty response from host")
if response_bytes[0] == 0x01:
raise HostFunctionError(response_bytes[1:].decode("utf-8"))
if response_bytes[0] != 0x00:
raise HostFunctionError("unknown response status")
if len(response_bytes) < 5:
raise HostFunctionError("malformed raw response: incomplete header")
ct_len = struct.unpack(">I", response_bytes[1:5])[0]
if len(response_bytes) < 5 + ct_len:
raise HostFunctionError("malformed raw response: content-type overflow")
content_type = response_bytes[5:5 + ct_len].decode("utf-8")
data = response_bytes[5 + ct_len:]
return content_type, data
if response.get("error"):
raise HostFunctionError(response["error"])
return SubsonicAPICallRawResult(
content_type=response.get("contentType", ""),
data=base64.b64decode(response.get("data", "")),
)
+2 -1
View File
@@ -12,6 +12,7 @@ from typing import Any
import extism
import json
import base64
class HostFunctionError(Exception):
@@ -134,7 +135,7 @@ Returns an error if the connection is not found or if sending fails.
"""
request = {
"connectionId": connection_id,
"data": data,
"data": base64.b64encode(data).decode("ascii"),
}
request_bytes = json.dumps(request).encode("utf-8")
request_mem = extism.memory.alloc(request_bytes)