Issue with experimental flag "remove_empty_subscription_responses"
Version Information
Server Version: 2.48.3
Environment
OSS
What is the current behaviour?
When enabling the experimental flag remove_empty_subscription_responses, and starting a subscription with no results, we get the following database query error:
FatalError cannot get array length of a non-array
Subscriptions with results don’t seem affected.
What is the expected behaviour?
We should get a response with an empty array as is the case without the flag.
How to reproduce the issue?
- Start Hasura with env variable
HASURA_GRAPHQL_EXPERIMENTAL_FEATURESset toremove_empty_subscription_responses - Make a subscription with filters so that there is no selected row
cannot get array length of a non-array
Please provide any traces or logs that could help here.
Log from such an error this morning
{
"detail": {
"connection_info": {
"msg": null,
"token_expiry": null,
"websocket_id": "c6d135c6-caa4-4d2d-a01b-c88a40778d02"
},
"event": {
"detail": {
"operation_id": "c30a0851-8cd8-4ee0-860a-be1faec429e0",
"operation_name": null,
"operation_type": {
"detail": {
"code": "unexpected",
"error": "database query error",
"internal": {
"arguments": [
"(Oid 705,Just (\"\\NUL\\NUL\\NUL\\SOH\\NUL\\NUL\\NUL\\NUL\\NUL\\NUL\\v\\134\\NUL\\NUL\\NUL\\SOH\\NUL\\NUL\\NUL\\SOH\\NUL\\NUL\\NUL\\DLE\\206\\ENQM#\\ETB\\167E\\DC4\\135<\\229\\175w`<\\143\",Binary))",
"(Oid 3807,Just (\"\\NUL\\NUL\\NUL\\SOH\\NUL\\NUL\\NUL\\NUL\\NUL\\NUL\\SO\\218\\NUL\\NUL\\NUL\\SOH\\NUL\\NUL\\NUL\\SOH\\NUL\\NUL\\NUL\\138\\SOH{\\\"cursor\\\":{},\\\"query\\\":{},\\\"session\\\":{\\\"x-hasura-user-id\\\":\\\"4\\\"},\\\"synthetic\\\":[\\\"0.0\\\",\\\"false\\\",\\\"{-1,5}\\\",\\\"4\\\",\\\"false\\\",\\\"true\\\",\\\"false\\\",\\\"REGULAR\\\",\\\"0\\\"]}\",Binary))"
],
"error": {
"description": null,
"exec_status": "FatalError",
"hint": null,
"message": "cannot get array length of a non-array",
"status_code": "22023"
},
"prepared": true,
"statement": "SELECT * FROM (SELECT \"__subs\".\"result_id\" , \"__fld_resp\".\"root\" AS \"result\" FROM UNNEST(($1)::uuid[], ($2)::json[]) AS \"__subs\"(\"result_id\", \"result_vars\") LEFT OUTER JOIN LATERAL (SELECT json_build_object('user_user_aggregate', \"_user_user_aggregate\".\"root\" ) AS \"root\" FROM (SELECT json_build_object('aggregate', json_build_object('count', COUNT(*) ) ) AS \"root\" FROM (SELECT 1 FROM (SELECT * FROM \"public\".\"user_user\" WHERE ((EXISTS (SELECT 1 FROM \"public\".\"user_user\" AS \"__exists_table_0\" WHERE ((((((\"__exists_table_0\".\"id\") = (((\"__subs\".\"result_vars\"#>>ARRAY['session', 'x-hasura-user-id']))::integer)) AND ('true')) AND ('true')) AND (((((\"__exists_table_0\".\"is_active\") = (('true')::boolean)) AND ('true')) AND ('true')) AND (((((\"__exists_table_0\".\"first_login\") = (('false')::boolean)) AND ('true')) AND ('true')) AND ('true')))) AND ('true')) )) AND ((EXISTS (SELECT 1 FROM \"public\".\"invoice_salary\" AS \"__be_1_invoice_salary\" WHERE ((((\"__be_1_invoice_salary\".\"payed_id\") = (\"public\".\"user_user\".\"id\")) AND ('true')) AND (((EXISTS (SELECT 1 FROM \"public\".\"user_user\" AS \"__exists_table_2\" WHERE ((((((\"__exists_table_2\".\"id\") = (((\"__subs\".\"result_vars\"#>>ARRAY['session', 'x-hasura-user-id']))::integer)) AND ('true')) AND ('true')) AND (((((\"__exists_table_2\".\"is_active\") = (('true')::boolean)) AND ('true')) AND ('true')) AND (((((\"__exists_table_2\".\"first_login\") = (('false')::boolean)) AND ('true')) AND ('true')) AND ('true')))) AND ('true')) )) AND ('true')) AND ((((\"__be_1_invoice_salary\".\"to_pay\") > (((\"__subs\".\"result_vars\"#>>ARRAY['synthetic', '0']))::numeric)) AND ('true')) AND ((EXISTS (SELECT 1 FROM \"public\".\"invoice_invoice\" AS \"__be_3_invoice_invoice\" WHERE ((((\"__be_3_invoice_invoice\".\"id\") = (\"__be_1_invoice_salary\".\"invoice_id\")) AND ('true')) AND (((EXISTS (SELECT 1 FROM \"public\".\"user_user\" AS \"__exists_table_4\" WHERE ((((((\"__exists_table_4\".\"id\") = (((\"__subs\".\"result_vars\"#>>ARRAY['session', 'x-hasura-user-id']))::integer)) AND ('true')) AND ('true')) AND (((((\"__exists_table_4\".\"is_active\") = (('true')::boolean)) AND ('true')) AND ('true')) AND (((((\"__exists_table_4\".\"first_login\") = (('false')::boolean)) AND ('true')) AND ('true')) AND ('true')))) AND ('true')) )) AND ('true')) AND ((((\"__be_3_invoice_invoice\".\"abandonned\") = (((\"__subs\".\"result_vars\"#>>ARRAY['synthetic', '1']))::boolean)) AND ('true')) AND ('true')))) )) AND (((NOT ((\"__be_1_invoice_salary\".\"state\") = ANY(((\"__subs\".\"result_vars\"#>>ARRAY['synthetic', '2']))::integer[]))) AND ('true')) AND ('true')))))) )) AND (((\"public\".\"user_user\".\"referent_id\") = (((\"__subs\".\"result_vars\"#>>ARRAY['synthetic', '3']))::integer)) AND ((EXISTS (SELECT 1 FROM \"public\".\"student_student\" AS \"__be_5_student_student\" WHERE ((((\"__be_5_student_student\".\"user_id\") = (\"public\".\"user_user\".\"id\")) AND ('true')) AND (((EXISTS (SELECT 1 FROM \"public\".\"user_user\" AS \"__exists_table_6\" WHERE ((((((\"__exists_table_6\".\"id\") = (((\"__subs\".\"result_vars\"#>>ARRAY['session', 'x-hasura-user-id']))::integer)) AND ('true')) AND ('true')) AND (((((\"__exists_table_6\".\"is_active\") = (('true')::boolean)) AND ('true')) AND ('true')) AND (((((\"__exists_table_6\".\"first_login\") = (('false')::boolean)) AND ('true')) AND ('true')) AND (((((\"__exists_table_6\".\"is_staff\") = (('true')::boolean)) AND ('true')) AND ('true')) AND ('true'))))) AND ('true')) )) AND ('true')) AND ((((\"__be_5_student_student\".\"mgp_nationality_blocked\") = (((\"__subs\".\"result_vars\"#>>ARRAY['synthetic', '4']))::boolean)) AND ('true')) AND ((((((\"__be_5_student_student\".\"need_sca_enroll\") = (((\"__subs\".\"result_vars\"#>>ARRAY['synthetic', '5']))::boolean)) AND ('true')) AND ('true')) OR (((((\"__be_5_student_student\".\"has_active_recipient\") = (((\"__subs\".\"result_vars\"#>>ARRAY['synthetic', '6']))::boolean)) AND ('true')) AND ('true')) OR ('false'))) AND ('true'))))) )) OR (EXISTS (SELECT 1 FROM (SELECT count(* ) AS \"mango_pay_naturaluser_count\" FROM \"public\".\"mango_pay_naturaluser\" AS \"__be_7_mango_pay_naturaluser\" WHERE (((\"__be_7_mango_pay_naturaluser\".\"user_id\") = (\"public\".\"user_user\".\"id\")) AND ((EXISTS (SELECT 1 FROM \"public\".\"user_user\" AS \"__exists_table_8\" WHERE ((((((\"__exists_table_8\".\"id\") = (((\"__subs\".\"result_vars\"#>>ARRAY['session', 'x-hasura-user-id']))::integer)) AND ('true')) AND ('true')) AND (((((\"__exists_table_8\".\"is_active\") = (('true')::boolean)) AND ('true')) AND ('true')) AND (((((\"__exists_table_8\".\"first_login\") = (('false')::boolean)) AND ('true')) AND ('true')) AND ('true')))) AND ('true')) )) AND ((\"__be_7_mango_pay_naturaluser\".\"kyc_level\") = (((\"__subs\".\"result_vars\"#>>ARRAY['synthetic', '7']))::\"mgp_naturaluser_kyc_level\")))) ) AS \"__sub\" WHERE ((\"__sub\".\"mango_pay_naturaluser_count\") = (((\"__subs\".\"result_vars\"#>>ARRAY['synthetic', '8']))::integer)) )))))) ) AS \"_root.base\" ) AS \"_root\" ) AS \"_user_user_aggregate\" ) AS \"__fld_resp\" ON ('true') ) AS \"__multiplex\" WHERE (EXISTS (SELECT 1 FROM \"json_each\"(\"result\") WHERE ((json_array_length(\"value\" )) > ('0')) )) "
},
"path": "$"
},
"type": "query_err"
},
"parameterized_query_hash": null,
"query": null,
"request_id": "3e76a5fb-fef1-4755-b1a8-a35d95c42155"
},
"type": "operation"
},
"user_vars": {
"x-hasura-role": "staff",
"x-hasura-user-id": "4"
}
},
"level": "error",
"timestamp": "2025-07-29T10:26:17.819+0000",
"type": "websocket-log"
}
Any possible solutions/workarounds you're aware of?
Disabling the feature makes the subscription work again, but we were hoping that it would reduce our Postgresql server load (see https://github.com/hasura/graphql-engine/issues/9994).
Extra note
I wanted to make a smaller subscription directly from the web console to provide it along a shorter log but GraphiQL doesn’t work because it tries to POST to https://v1/graphql, ignoring the domain name.
I am unsure whether this warrant a new issue (the flag has no incidence on it) or is a problem on my end (perhaps a faulty browser cache)
Thanks, @ko-pp — let me share this internally and see what folks have to say. Glad that disabling it works, but totally understand that you're looking to optimize perf here 👍
@ko-pp thanks for the report, I'll look into this.
To save time triaging, are you getting this with streaming subscriptions (the ones that use a cursor), or regular subscription/live-queries? If you can post an anonymized version of your graphql query that might help as well
Hello, it’s a regular subscription
Here is the full query used to get the error in the first message
subscription {
user_user(
order_by: {id: desc}
where: {
salaries: {
state: {_nin: [-1, 5]}
to_pay: {_gt: 0}
invoice: {
abandonned: {_eq: false}
}
},
_or: [
{
student: {
mgp_nationality_blocked: {_eq: false},
_or: [
{ need_sca_enroll: { _eq: true } },
{ has_active_recipient: { _eq: false } }
]
}
},
{
natural_users_aggregate: {
count: {
predicate: { _eq: 0 },
filter: {
kyc_level: { _eq: "REGULAR" }
}
}
}
}
]
}
) {
id
first_name
last_name
user_hubspot_id
salaries (
where: {
state: {_nin: [-1, 5]},
invoice: {
abandonned: {_eq: false}
}
},
order_by: {
invoice: {period_start: asc}
},
limit: 1
) {
invoice {
period_start
}
}
salaries_aggregate(
where: {
state: {_nin: [-1, 5]}
invoice: {
abandonned: {_eq: false}
}
}
) {
aggregate {
sum {
to_pay
}
}
}
natural_users(
order_by: {created_at: desc},
limit: 1
) {
flows_blocked_code
inflows_blocked
natural_id
outflows_blocked
kyc_documents(
order_by: {created_at: desc},
limit: 1
) {
created_at
document_id
refused_reason_message
refused_reason_type
status
}
}
student {
need_sca_enroll
has_active_recipient
}
}
}
I also tried setting up a simpler query but I get neither a response nor errors in the logs for this one:
subscription {
user_user(where: {id: {_eq: 0}}) {
id
}
}
I do get a response with an empty array for both queries without the flag.
but I get neither a response nor errors in the logs for this one ... I do get a response with an empty array for both queries without the flag.
This is expected behavior, although I think we need to document it better. Empty result sets will never be returned. This should work as expected for streaming subscriptions but might not be desirable for regular subscriptions (you won't notice when a subscription goes from returning some result to an empty result, for instance).
Leaving aside the bug you originally reported, does this behavior actually work for your use case? i.e. is it still useful for you, or desired, for the subscriber to never get an updated response when empty?
is it still useful for you, or desired, for the subscriber to never get an updated response when empty?
The use-case for the big query is to list issues to be resolved on a dashboard for our staff (it’s one of several); we need both
- an empty initial response if there is no issue to be fixed as we wouldn’t otherwise easily know if there is nothing to display or if the connection/server is slow
- an empty array response when all issues are resolved to empty the displayed list
If not returning at all is expected behavior, we won’t be using this flag and either endure the spikes of DB usage whenever someone is on the dashboard, or rework it (as well as other subscriptions) to use periodical queries instead 🤔
I see. And it doesn't sound like a streaming subscription would work for your dashboard either.
We have a fix for the bug originally reported, but unfortunately won't be able to get rid of those two caveats with regular subscriptions at this time. The trouble is we rely on postgres to construct the json response as the client will receive it, and at the point in the codebase that we receive the database response we don't have things like alias and field type (array or scalar/object) available for reconstructing the "empty" responses ([] or null).
Not a satisfying answer but one workaround might be to store a special fake user which you filter out in your dashboard logic