varlink: omit empty parameters field in JSON messages (#38922)

When varlink parameters are empty, omit the "parameters" field entirely
rather than sending "parameters":{}. This reduces message size and
follows varlink specification which allows parameters to be omitted.

The implementation supports three equivalent representations for empty
parameters: field omission, JSON null, and empty object {}. All three
are accepted on input for backward compatibility.

Fixes: #38474
This commit is contained in:
Govind Venugopal
2025-10-16 08:06:17 -07:00
committed by GitHub
parent f102bc3e5f
commit 48c64813ec
7 changed files with 109 additions and 95 deletions

View File

@@ -44,6 +44,13 @@
<citerefentry><refentrytitle>sd-json</refentrytitle><manvolnum>3</manvolnum></citerefentry> API for JSON
serialization, deserialization and manipulation.</para>
<para>Canonical encoding rules: sd-varlink omits the <literal>"parameters"</literal> member on the wire in replies,
errors, and notifications when there are no parameters to transmit. This reduces message size and
avoids ambiguity. Receivers must be tolerant and accept any of the following encodings for the
absence of parameters: an omitted <literal>"parameters"</literal> key (preferred), a JSON <literal>null</literal>
value, or an empty object <literal>{}</literal>. When decoding, sd-varlink treats JSON <literal>null</literal>
as if the member was omitted.</para>
<para>The <citerefentry><refentrytitle>varlinkctl</refentrytitle><manvolnum>1</manvolnum></citerefentry> tool
makes the functionality implemented by sd-varlink available from the command line.</para>
</refsect1>

View File

@@ -170,6 +170,7 @@ enum {
_JSON_BUILD_PAIR_STRV_NON_EMPTY,
_JSON_BUILD_PAIR_STRV_ENV_PAIR_NON_EMPTY,
_JSON_BUILD_PAIR_VARIANT_NON_NULL,
_JSON_BUILD_PAIR_VARIANT_NON_EMPTY,
/* _SD_JSON_BUILD_PAIR_VARIANT_ARRAY_NON_EMPTY, */
_JSON_BUILD_PAIR_BYTE_ARRAY_NON_EMPTY,
_JSON_BUILD_PAIR_IN4_ADDR_NON_NULL,
@@ -218,6 +219,7 @@ enum {
#define JSON_BUILD_PAIR_STRV_NON_EMPTY(name, l) _JSON_BUILD_PAIR_STRV_NON_EMPTY, (const char*) { name }, (char**) { l }
#define JSON_BUILD_PAIR_STRV_ENV_PAIR_NON_EMPTY(name, l) _JSON_BUILD_PAIR_STRV_ENV_PAIR_NON_EMPTY, (const char*) { name }, (char**) { l }
#define JSON_BUILD_PAIR_VARIANT_NON_NULL(name, v) _JSON_BUILD_PAIR_VARIANT_NON_NULL, (const char*) { name }, (sd_json_variant*) { v }
#define JSON_BUILD_PAIR_VARIANT_NON_EMPTY(name, v) _JSON_BUILD_PAIR_VARIANT_NON_EMPTY, (const char*) { name }, (sd_json_variant*) { v }
#define JSON_BUILD_PAIR_BYTE_ARRAY_NON_EMPTY(name, v, n) _JSON_BUILD_PAIR_BYTE_ARRAY_NON_EMPTY, (const char*) { name }, (const void*) { v }, (size_t) { n }
#define JSON_BUILD_PAIR_IN4_ADDR_NON_NULL(name, v) _JSON_BUILD_PAIR_IN4_ADDR_NON_NULL, (const char*) { name }, (const struct in_addr*) { v }
#define JSON_BUILD_PAIR_IN6_ADDR_NON_NULL(name, v) _JSON_BUILD_PAIR_IN6_ADDR_NON_NULL, (const char*) { name }, (const struct in6_addr*) { v }

View File

@@ -4445,7 +4445,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4475,7 +4475,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4503,7 +4503,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4530,7 +4530,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4567,7 +4567,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4593,7 +4593,33 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
add_more = sd_json_variant_ref(v);
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
}
case _JSON_BUILD_PAIR_VARIANT_NON_EMPTY: {
sd_json_variant *v;
const char *n;
if (current->expect != EXPECT_OBJECT_KEY) {
r = -EINVAL;
goto finish;
}
n = va_arg(ap, const char *);
v = va_arg(ap, sd_json_variant *);
if (v && !sd_json_variant_is_blank_object(v) && current->n_suppress == 0) {
r = sd_json_variant_new_string(&add, n);
if (r < 0)
goto finish;
add_more = sd_json_variant_ref(v);
}
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4623,7 +4649,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4651,7 +4677,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4679,7 +4705,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4709,7 +4735,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4737,7 +4763,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4765,7 +4791,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4795,7 +4821,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4825,7 +4851,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4887,7 +4913,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
}
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4923,7 +4949,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;
@@ -4951,7 +4977,7 @@ _public_ int sd_json_buildv(sd_json_variant **ret, va_list ap) {
goto finish;
}
n_subtract = 2; /* we generated two item */
n_subtract = 2; /* we generated two items */
current->expect = EXPECT_OBJECT_KEY;
break;

View File

@@ -5,6 +5,7 @@
#include "alloc-util.h"
#include "ansi-color.h"
#include "extract-word.h"
#include "json-internal.h"
#include "json-util.h"
#include "log.h"
#include "memstream-util.h"
@@ -1793,11 +1794,9 @@ static int varlink_idl_validate_symbol(const sd_varlink_symbol *symbol, sd_json_
assert(symbol);
assert(!IN_SET(symbol->symbol_type, _SD_VARLINK_SYMBOL_COMMENT, _SD_VARLINK_INTERFACE_COMMENT));
if (!v) {
if (reterr_bad_field)
*reterr_bad_field = NULL;
return varlink_idl_log(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Null object passed, refusing.");
}
/* Consider a NULL pointer equivalent to an empty object */
if (!v)
v = JSON_VARIANT_MAGIC_EMPTY_OBJECT;
switch (symbol->symbol_type) {

View File

@@ -1020,6 +1020,7 @@ static int varlink_test_timeout(sd_varlink *v) {
}
static int varlink_dispatch_local_error(sd_varlink *v, const char *error) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *empty = NULL;
int r;
assert(v);
@@ -1028,7 +1029,11 @@ static int varlink_dispatch_local_error(sd_varlink *v, const char *error) {
if (!v->reply_callback)
return 0;
r = v->reply_callback(v, NULL, error, SD_VARLINK_REPLY_ERROR|SD_VARLINK_REPLY_LOCAL, v->userdata);
r = sd_json_variant_new_object(&empty, NULL, 0);
if (r < 0)
return r;
r = v->reply_callback(v, empty, error, SD_VARLINK_REPLY_ERROR|SD_VARLINK_REPLY_LOCAL, v->userdata);
if (r < 0)
varlink_log_errno(v, r, "Reply callback returned error, ignoring: %m");
@@ -1061,25 +1066,23 @@ static int varlink_dispatch_disconnect(sd_varlink *v) {
return 1;
}
static int varlink_sanitize_parameters(sd_json_variant **v) {
static int varlink_sanitize_incoming_parameters(sd_json_variant **v) {
int r;
assert(v);
/* Varlink always wants a parameters list, hence make one if the caller doesn't want any */
if (!*v)
return sd_json_variant_new_object(v, NULL, 0);
if (sd_json_variant_is_null(*v)) {
sd_json_variant *empty;
/* Convert NULL or JSON null to empty object for method handlers (backward compatibility) */
if (!*v || sd_json_variant_is_null(*v)) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *empty = NULL;
r = sd_json_variant_new_object(&empty, NULL, 0);
if (r < 0)
return r;
/* sd_json_variant_unref() is a NOP if *v is NULL */
sd_json_variant_unref(*v);
*v = empty;
*v = TAKE_PTR(empty);
return 0;
}
/* Ensure we have an object */
if (!sd_json_variant_is_object(*v))
return -EINVAL;
@@ -1146,7 +1149,7 @@ static int varlink_dispatch_reply(sd_varlink *v) {
if (error && FLAGS_SET(flags, SD_VARLINK_REPLY_CONTINUES))
goto invalid;
r = varlink_sanitize_parameters(&parameters);
r = varlink_sanitize_incoming_parameters(&parameters);
if (r < 0)
goto invalid;
@@ -1327,7 +1330,7 @@ static int varlink_dispatch_method(sd_varlink *v) {
if (!method)
goto invalid;
r = varlink_sanitize_parameters(&parameters);
r = varlink_sanitize_incoming_parameters(&parameters);
if (r < 0)
goto fail;
@@ -1575,13 +1578,14 @@ _public_ int sd_varlink_get_current_parameters(sd_varlink *v, sd_json_variant **
if (!v->current)
return -ENODATA;
if (!ret)
return 0;
p = sd_json_variant_by_key(v->current, "parameters");
if (!p)
return -ENODATA;
if (ret)
*ret = sd_json_variant_ref(p);
if (!p || sd_json_variant_is_null(p))
return sd_json_variant_new_object(ret, NULL, 0);
*ret = sd_json_variant_ref(p);
return 0;
}
@@ -2024,14 +2028,10 @@ _public_ int sd_varlink_send(sd_varlink *v, const char *method, sd_json_variant
if (!IN_SET(v->state, VARLINK_IDLE_CLIENT, VARLINK_AWAITING_REPLY))
return varlink_log_errno(v, SYNTHETIC_ERRNO(EBUSY), "Connection busy.");
r = varlink_sanitize_parameters(&parameters);
if (r < 0)
return varlink_log_errno(v, r, "Failed to sanitize parameters: %m");
r = sd_json_buildo(
&m,
SD_JSON_BUILD_PAIR("method", SD_JSON_BUILD_STRING(method)),
SD_JSON_BUILD_PAIR("parameters", SD_JSON_BUILD_VARIANT(parameters)),
JSON_BUILD_PAIR_VARIANT_NON_EMPTY("parameters", parameters),
SD_JSON_BUILD_PAIR("oneway", SD_JSON_BUILD_BOOLEAN(true)));
if (r < 0)
return varlink_log_errno(v, r, "Failed to build json message: %m");
@@ -2076,14 +2076,10 @@ _public_ int sd_varlink_invoke(sd_varlink *v, const char *method, sd_json_varian
if (!IN_SET(v->state, VARLINK_IDLE_CLIENT, VARLINK_AWAITING_REPLY))
return varlink_log_errno(v, SYNTHETIC_ERRNO(EBUSY), "Connection busy.");
r = varlink_sanitize_parameters(&parameters);
if (r < 0)
return varlink_log_errno(v, r, "Failed to sanitize parameters: %m");
r = sd_json_buildo(
&m,
SD_JSON_BUILD_PAIR("method", SD_JSON_BUILD_STRING(method)),
SD_JSON_BUILD_PAIR("parameters", SD_JSON_BUILD_VARIANT(parameters)));
JSON_BUILD_PAIR_VARIANT_NON_EMPTY("parameters", parameters));
if (r < 0)
return varlink_log_errno(v, r, "Failed to build json message: %m");
@@ -2130,14 +2126,10 @@ _public_ int sd_varlink_observe(sd_varlink *v, const char *method, sd_json_varia
if (v->state != VARLINK_IDLE_CLIENT)
return varlink_log_errno(v, SYNTHETIC_ERRNO(EBUSY), "Connection busy.");
r = varlink_sanitize_parameters(&parameters);
if (r < 0)
return varlink_log_errno(v, r, "Failed to sanitize parameters: %m");
r = sd_json_buildo(
&m,
SD_JSON_BUILD_PAIR("method", SD_JSON_BUILD_STRING(method)),
SD_JSON_BUILD_PAIR("parameters", SD_JSON_BUILD_VARIANT(parameters)),
JSON_BUILD_PAIR_VARIANT_NON_EMPTY("parameters", parameters),
SD_JSON_BUILD_PAIR("more", SD_JSON_BUILD_BOOLEAN(true)));
if (r < 0)
return varlink_log_errno(v, r, "Failed to build json message: %m");
@@ -2195,14 +2187,10 @@ _public_ int sd_varlink_call_full(
* that we can assign a new reply shortly. */
varlink_clear_current(v);
r = varlink_sanitize_parameters(&parameters);
if (r < 0)
return varlink_log_errno(v, r, "Failed to sanitize parameters: %m");
r = sd_json_buildo(
&m,
SD_JSON_BUILD_PAIR("method", SD_JSON_BUILD_STRING(method)),
SD_JSON_BUILD_PAIR("parameters", SD_JSON_BUILD_VARIANT(parameters)));
JSON_BUILD_PAIR_VARIANT_NON_EMPTY("parameters", parameters));
if (r < 0)
return varlink_log_errno(v, r, "Failed to build json message: %m");
@@ -2353,14 +2341,10 @@ _public_ int sd_varlink_collect_full(
* that we can assign a new reply shortly. */
varlink_clear_current(v);
r = varlink_sanitize_parameters(&parameters);
if (r < 0)
return varlink_log_errno(v, r, "Failed to sanitize parameters: %m");
r = sd_json_buildo(
&m,
SD_JSON_BUILD_PAIR("method", SD_JSON_BUILD_STRING(method)),
SD_JSON_BUILD_PAIR("parameters", SD_JSON_BUILD_VARIANT(parameters)),
JSON_BUILD_PAIR_VARIANT_NON_EMPTY("parameters", parameters),
SD_JSON_BUILD_PAIR("more", SD_JSON_BUILD_BOOLEAN(true)));
if (r < 0)
return varlink_log_errno(v, r, "Failed to build json message: %m");
@@ -2501,14 +2485,7 @@ _public_ int sd_varlink_reply(sd_varlink *v, sd_json_variant *parameters) {
VARLINK_PENDING_METHOD, VARLINK_PENDING_METHOD_MORE))
return -EBUSY;
r = varlink_sanitize_parameters(&parameters);
if (r < 0)
return varlink_log_errno(v, r, "Failed to sanitize parameters: %m");
r = sd_json_buildo(&m, SD_JSON_BUILD_PAIR("parameters", SD_JSON_BUILD_VARIANT(parameters)));
if (r < 0)
return varlink_log_errno(v, r, "Failed to build json message: %m");
/* Validate parameters BEFORE sanitization */
if (v->current_method) {
const char *bad_field = NULL;
@@ -2519,6 +2496,10 @@ _public_ int sd_varlink_reply(sd_varlink *v, sd_json_variant *parameters) {
v->current_method->name, strna(bad_field));
}
r = sd_json_buildo(&m, JSON_BUILD_PAIR_VARIANT_NON_EMPTY("parameters", parameters));
if (r < 0)
return varlink_log_errno(v, r, "Failed to build json message: %m");
r = varlink_enqueue_json(v, m);
if (r < 0)
return varlink_log_errno(v, r, "Failed to enqueue json message: %m");
@@ -2588,17 +2569,7 @@ _public_ int sd_varlink_error(sd_varlink *v, const char *error_id, sd_json_varia
* the callers don't need to do this explicitly. */
sd_varlink_reset_fds(v);
r = varlink_sanitize_parameters(&parameters);
if (r < 0)
return varlink_log_errno(v, r, "Failed to sanitize parameters: %m");
r = sd_json_buildo(
&m,
SD_JSON_BUILD_PAIR("error", SD_JSON_BUILD_STRING(error_id)),
SD_JSON_BUILD_PAIR("parameters", SD_JSON_BUILD_VARIANT(parameters)));
if (r < 0)
return varlink_log_errno(v, r, "Failed to build json message: %m");
/* Validate parameters BEFORE sanitization */
sd_varlink_symbol *symbol = hashmap_get(v->server->symbols, error_id);
if (!symbol)
varlink_log(v, "No interface description defined for error '%s', not validating.", error_id);
@@ -2612,6 +2583,13 @@ _public_ int sd_varlink_error(sd_varlink *v, const char *error_id, sd_json_varia
error_id, strna(bad_field));
}
r = sd_json_buildo(
&m,
SD_JSON_BUILD_PAIR("error", SD_JSON_BUILD_STRING(error_id)),
JSON_BUILD_PAIR_VARIANT_NON_EMPTY("parameters", parameters));
if (r < 0)
return varlink_log_errno(v, r, "Failed to build json message: %m");
r = varlink_enqueue_json(v, m);
if (r < 0)
return varlink_log_errno(v, r, "Failed to enqueue json message: %m");
@@ -2726,17 +2704,7 @@ _public_ int sd_varlink_notify(sd_varlink *v, sd_json_variant *parameters) {
if (!IN_SET(v->state, VARLINK_PROCESSING_METHOD_MORE, VARLINK_PENDING_METHOD_MORE))
return varlink_log_errno(v, SYNTHETIC_ERRNO(EBUSY), "Connection busy.");
r = varlink_sanitize_parameters(&parameters);
if (r < 0)
return varlink_log_errno(v, r, "Failed to sanitize parameters: %m");
r = sd_json_buildo(
&m,
SD_JSON_BUILD_PAIR("parameters", SD_JSON_BUILD_VARIANT(parameters)),
SD_JSON_BUILD_PAIR("continues", SD_JSON_BUILD_BOOLEAN(true)));
if (r < 0)
return varlink_log_errno(v, r, "Failed to build json message: %m");
/* Validate parameters BEFORE sanitization */
if (v->current_method) {
const char *bad_field = NULL;
@@ -2750,6 +2718,13 @@ _public_ int sd_varlink_notify(sd_varlink *v, sd_json_variant *parameters) {
v->current_method->name, strna(bad_field));
}
r = sd_json_buildo(
&m,
JSON_BUILD_PAIR_VARIANT_NON_EMPTY("parameters", parameters),
SD_JSON_BUILD_PAIR("continues", SD_JSON_BUILD_BOOLEAN(true)));
if (r < 0)
return varlink_log_errno(v, r, "Failed to build json message: %m");
r = varlink_enqueue_json(v, m);
if (r < 0)
return varlink_log_errno(v, r, "Failed to enqueue json message: %m");
@@ -2783,6 +2758,7 @@ _public_ int sd_varlink_dispatch(sd_varlink *v, sd_json_variant *parameters, con
/* A wrapper around json_dispatch_full() that returns a nice InvalidParameter error if we hit a problem with some field. */
/* sd_json_dispatch_full() now handles NULL parameters gracefully */
r = sd_json_dispatch_full(parameters, dispatch_table, /* bad= */ NULL, /* flags= */ 0, userdata, &bad_field);
if (r < 0) {
if (bad_field)

View File

@@ -47,6 +47,7 @@ int varlink_method_ping(sd_varlink *link, sd_json_variant *parameters, sd_varlin
int r;
assert(link);
assert(parameters);
r = sd_varlink_dispatch(link, parameters, /* dispatch_table= */ NULL, /* userdata= */ NULL);
if (r != 0)

View File

@@ -205,6 +205,9 @@ static int overload_reply(sd_varlink *link, sd_json_variant *parameters, const c
log_debug("Over reply triggered with error: %s", strna(error_id));
ASSERT_STREQ(error_id, SD_VARLINK_ERROR_DISCONNECTED);
/* Local disconnect errors carry empty parameters. Ensure we propagate
* a consistent empty object for API reliability. */
ASSERT_TRUE(sd_json_variant_is_blank_object(parameters));
sd_event_exit(sd_varlink_get_event(link), 0);
return 0;