Commit a596c215df for asterisk.org
commit a596c215dfbc1a5ef85cd02b61ff93222e6f8459
Author: George Joseph <gjoseph@sangoma.com>
Date: Thu Sep 3 05:25:25 2026 -0600
ARI, chan_websocket, res_websocket_client: Handle non-blocking and timeout correctly.
This change was prompted by an issue where if the websocket peer to
which ARI or chan_websocket is connected stops consuming TCP packets,
its TCP receive buffer will begin to fill and ultimately cause the
client's TCP send buffer to start filling. When the send buffer is
completely filled, further writes to the websocket will block. This can
cause a cascading lock situation with chan_websocket because it sends
large amounts of data very quickly but can also affect ARI.
* Added "write_timeout" parameters to websocket_client.conf and
chan_websocket.conf. ari.conf already had the
"websocket_write_timeout" parameter. See notes below.
* Updated chan_websocket to use the write_timeout set in
chan_websocket.conf for incoming/server connections if set and to use
AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT (100ms) as the default if not set.
* Updated chan_websocket to use the write_timeout set in
websocket_client.conf for outgoing/client connections if set and to use
write_timeout from chan_websocket.conf if not set. If neither is set,
AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT (100ms) is used as the default.
* Set non-blocking mode on the websocket in chan_websocket for both
client and server connections. ARI already sets it. This is required
for the timeouts to operate correctly.
* Updated sample config files and XML documentation for all 3 areas.
Resolves: #2140
UpgradeNote: A new "write_timeout" parameter has been added to
websocket_client.conf that allows setting the maximum amount of time a
write operation can take before returning an error.
UpgradeNote: A new "write_timeout" parameter has been added to
chan_websocket.conf setting the maximum amount of time a write operation
can take before returning an error. Outgoing/client connections can
override this with the new "write_timeout" parameter that was also added
to websocket_client.conf.
DeveloperNote: A new "write_timeout" field has been added to the
ast_websocket_client_options structure that allows setting the maximum
amount of time a write operation can take before returning an error.
The websocket session must be set to nonblocking for this to take effect.
diff --git a/channels/chan_websocket.c b/channels/chan_websocket.c
index 724c9263fb..2667a81c10 100644
--- a/channels/chan_websocket.c
+++ b/channels/chan_websocket.c
@@ -68,6 +68,7 @@ static const char *msg_format_map[] = {
struct webchan_conf_global {
SORCERY_OBJECT(details);
enum webchan_control_msg_format control_msg_format;
+ int write_timeout;
};
/* This is from the perspective of the app, NOT Asterisk */
@@ -534,10 +535,15 @@ static struct ast_frame *dequeue_frame(struct websocket_pvt *instance)
* We just need to send the data to the websocket.
* The data should already be NULL terminated.
*/
- ast_websocket_write_string(instance->websocket,
+ int res = ast_websocket_write_string(instance->websocket,
queued_frame->data.ptr);
- ast_debug(4, "%s: Sent %s\n",
- ast_channel_name(instance->channel), (char *)queued_frame->data.ptr);
+ if (res != 0) {
+ ast_log(LOG_ERROR, "%s: Unable to send event %s\n",
+ ast_channel_name(instance->channel), (char *)queued_frame->data.ptr);
+ } else {
+ ast_debug(4, "%s: Sent %s\n",
+ ast_channel_name(instance->channel), (char *)queued_frame->data.ptr);
+ }
}
/*
* We do NOT send these to the core so we need to free
@@ -1213,6 +1219,14 @@ static int websocket_handoff_to_channel(struct websocket_pvt *instance)
ast_log(LOG_WARNING, "Failed to set TCP_NODELAY on websocket connection: %s\n", strerror(errno));
}
+ /*
+ * The way write timeouts are handled in iostream requires the socket to be
+ * in non-blocking mode. This is fine for reads as well because we already
+ * set the websocket file descriptor on the channel and let it call
+ * webchan_read() when data is available.
+ */
+ ast_websocket_set_nonblock(instance->websocket);
+
/*
* Tell res_http_websocket to accumulate incoming WebSocket CONTINUATION frames
* into chunks of 1024 bytes and send us a TEXT or BINARY frame when the threshold
@@ -1266,6 +1280,7 @@ static void _websocket_request_hangup(struct websocket_pvt *instance, int ast_ca
static int webchan_write(struct ast_channel *ast, struct ast_frame *f)
{
struct websocket_pvt *instance = ast_channel_tech_pvt(ast);
+ int res = 0;
if (!instance || !instance->websocket) {
ast_log(LOG_WARNING, "%s: WebSocket instance or client not found\n",
@@ -1295,8 +1310,13 @@ static int webchan_write(struct ast_channel *ast, struct ast_frame *f)
return -1;
}
- return ast_websocket_write(instance->websocket, AST_WEBSOCKET_OPCODE_BINARY,
+ res = ast_websocket_write(instance->websocket, AST_WEBSOCKET_OPCODE_BINARY,
(char *)f->data.ptr, (uint64_t)f->datalen);
+ if (res != 0) {
+ ast_log(LOG_WARNING, "%s: WebSocket write failure\n", ast_channel_name(ast));
+ }
+
+ return res;
}
/*!
@@ -1311,6 +1331,10 @@ static int webchan_call(struct ast_channel *ast, const char *dest,
{
struct websocket_pvt *instance = ast_channel_tech_pvt(ast);
enum ast_websocket_result result;
+ struct webchan_conf_global *global_cfg = ast_sorcery_retrieve_by_id(sorcery, "global", "global");
+ int global_write_timeout = global_cfg ? global_cfg->write_timeout : AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT;
+
+ ao2_cleanup(global_cfg);
if (!instance) {
ast_log(LOG_WARNING, "%s: WebSocket instance not found\n",
@@ -1348,6 +1372,21 @@ static int webchan_call(struct ast_channel *ast, const char *dest,
return -1;
}
+ /*
+ * If websocket_client->write_timeout was set in websocket_client.conf, it will
+ * have been applied to the websocket by ast_websocket_client_connect() above.
+ * If it wasn't set in websocket_client.conf, the value will be INT_MAX and
+ * and ast_websocket_client_connect() will have set AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT
+ * on the websocket. However, the user may have set write_timeout in the global section
+ * of chan_websocket.conf so if it wasn't set in websocket_client.conf, we'll now set
+ * the websocket timeout to that. If they haven't set it in chan_websocket.conf either,
+ * it'll default to AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT as well so the call below will
+ * basically become a no-op.
+ */
+ if (instance->client->write_timeout == INT_MAX) {
+ ast_websocket_set_timeout(instance->websocket, global_write_timeout);
+ }
+
return websocket_handoff_to_channel(instance);
}
@@ -1877,6 +1916,10 @@ static void incoming_ws_established_cb(struct ast_websocket *ast_ws_session,
struct ast_variable *v;
const char *connection_id = NULL;
struct websocket_pvt *instance = NULL;
+ struct webchan_conf_global *global_cfg = ast_sorcery_retrieve_by_id(sorcery, "global", "global");
+ int global_write_timeout = global_cfg ? global_cfg->write_timeout : AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT;
+
+ ao2_cleanup(global_cfg);
ast_debug(3, "WebSocket established\n");
@@ -1915,6 +1958,8 @@ static void incoming_ws_established_cb(struct ast_websocket *ast_ws_session,
}
instance->websocket = ao2_bump(ast_ws_session);
+ ast_websocket_set_timeout(instance->websocket, global_write_timeout);
+
websocket_handoff_to_channel(instance);
ao2_cleanup(instance);
/*
@@ -2066,6 +2111,11 @@ static int global_apply(const struct ast_sorcery *sorcery, void *obj)
ast_debug(1, "control_msg_format: %s\n",
control_msg_format_to_str(cfg->control_msg_format));
+ if (cfg->write_timeout <= 0) {
+ ast_log(LOG_WARNING, "The write_timeout parameter must be > 0\n");
+ return -1;
+ }
+
return 0;
}
@@ -2090,6 +2140,8 @@ static int load_config(void)
ast_sorcery_object_field_register_nodoc(sorcery, "global", "type", "", OPT_NOOP_T, 0, 0);
ast_sorcery_register_cust(global, control_message_format, "plain-text");
+ ast_sorcery_register_int(global, webchan_conf_global, write_timeout, write_timeout,
+ AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT);
ast_sorcery_load(sorcery);
diff --git a/channels/chan_websocket_doc.xml b/channels/chan_websocket_doc.xml
index ca836a026e..a0714feff5 100644
--- a/channels/chan_websocket_doc.xml
+++ b/channels/chan_websocket_doc.xml
@@ -35,6 +35,34 @@
</enumlist>
</description>
</configOption>
+ <configOption name="write_timeout" default="100">
+ <since>
+ <version>20.22.0</version>
+ <version>22.12.0</version>
+ <version>23.6.0</version>
+ <version>24.0.0</version>
+ </since>
+ <synopsis>Write timeout (ms).</synopsis>
+ <description>
+ <para>
+ The maximum number of milliseconds to wait for a write
+ to the WebSocket to succeed. The WebSocket will be closed and the
+ channel hung up when the timeout is reached. For outgoing
+ WebSocket connections, this sets the default timeout which can be
+ overridden in websocket_client.conf.
+ </para>
+ <note>
+ <para>
+ Success means the write request was accepted by the
+ operating system and does not imply the payload was
+ actually transmitted or received by the server.
+ </para>
+ </note>
+ </description>
+ <see-also>
+ <ref type="configOption" module="res_websocket_client">write_timeout</ref>
+ </see-also>
+ </configOption>
</configObject>
</configFile>
</configInfo>
diff --git a/configs/samples/ari.conf.sample b/configs/samples/ari.conf.sample
index 04973e10b4..6fc3f40e29 100644
--- a/configs/samples/ari.conf.sample
+++ b/configs/samples/ari.conf.sample
@@ -10,8 +10,9 @@ enabled = yes ; When set to no, ARI support is disabled.
;
; Default write timeout to set on websockets. This value may need to be adjusted
; for connections where Asterisk must write a substantial amount of data and the
-; receiving clients are slow to process the received information. Value is in
-; milliseconds; default is 100 ms.
+; receiving clients are slow to process the received information. Outgoing WebSocket
+; connections can override this in websocket_client.conf.
+; Value is in milliseconds; default is 100 ms.
;websocket_write_timeout = 100
;
; Display certain channel variables every time a channel-oriented
diff --git a/configs/samples/chan_websocket.conf.sample b/configs/samples/chan_websocket.conf.sample
index bae06f9541..5aacb5b755 100644
--- a/configs/samples/chan_websocket.conf.sample
+++ b/configs/samples/chan_websocket.conf.sample
@@ -8,3 +8,15 @@
; json: All messages are properly formatted
; JSON.
; Default: plain-text
+
+;write_timeout = 100 ; Write timeout in milliseconds.
+ ; The maximum number of milliseconds to wait for a write
+ ; to the WebSocket to succeed. The WebSocket will be closed
+ ; and the channel hung up when the timeout is reached.
+ ; Outgoing WebSocket connections can override this in
+ ; websocket_client.conf.
+ ; NOTE: Success means the write request was accepted by the
+ ; operating system and does not imply the payload was
+ ; actually transmitted or received by the server.
+ ; Default: 100
+
\ No newline at end of file
diff --git a/configs/samples/websocket_client.conf.sample b/configs/samples/websocket_client.conf.sample
index 706646becf..7df1d745cd 100644
--- a/configs/samples/websocket_client.conf.sample
+++ b/configs/samples/websocket_client.conf.sample
@@ -14,6 +14,10 @@
;password = password ; The authentication password for the username.
; Default: none
;connection_timeout = 500 ; Connection timeout in milliseconds.
+ ; The maximum number of milliseconds to wait for a connection
+ ; to the WebSocket server to succeed. If the timer expires,
+ ; reconnection will be attempted based on the settings of the
+ ; reconnect_attempts and reconnect_interval parameters.
; Default: 500
;reconnect_interval = 1000 ; Number of milliseconds between (re)connection
; attempts.
@@ -24,6 +28,15 @@
; always retry forever but this setting will control
; how often failure messages are logged.
; Default: 4 for both connection types.
+;write_timeout = 100 ; Write timeout in milliseconds.
+ ; The maximum number of milliseconds to wait for a write
+ ; to the WebSocket to succeed. If this connection is used
+ ; by chan_websocket, the WebSocket will be closed and the
+ ; channel hung up when the timeout is reached.
+ ; NOTE: Success means the write request was accepted by the
+ ; operating system and does not imply the payload was
+ ; actually transmitted or received by the server.
+ ; Default: 100
;tls_enabled = no ; Set to "yes" to enable TLS connections.
; Default: no
;ca_list_file = /etc/pki/tls/cert.pem
diff --git a/include/asterisk/http_websocket.h b/include/asterisk/http_websocket.h
index 8e542b8a9a..54e0910132 100644
--- a/include/asterisk/http_websocket.h
+++ b/include/asterisk/http_websocket.h
@@ -548,6 +548,14 @@ struct ast_websocket_client_options {
int pingpongs; /*!< Enable Websocket PING/PONGs */
unsigned int pingpong_interval; /*!< Send PING messages at this interval in seconds */
unsigned int pingpong_probes; /*!< Close connection after this many missed responses */
+ /*!
+ * Optional write timeout
+ *
+ * How long (in milliseconds) to wait for a write to the websocket to complete.
+ * \warning This parameter is ignored if the WebSocket is in blocking mode.
+ * Ensure ast_websocket_set_nonblock() is called before calling ast_websocket_write().
+ */
+ int write_timeout;
};
/*!
@@ -589,6 +597,11 @@ AST_OPTIONAL_API(const char *, ast_websocket_client_accept_protocol,
* \since 11.11.0
* \since 12.4.0
*
+ * \warning To be effective, the socket must be in non-blocking mode because the timeout
+ * can only be checked after a read or write operation returns. If the socket is in blocking
+ * mode (the default), those calls may block for longer than the specified timeout, possibly
+ * much longer.
+ *
* \retval 0 on success
* \retval -1 on failure
*/
diff --git a/include/asterisk/iostream.h b/include/asterisk/iostream.h
index 9d88e3922f..a0570559a4 100644
--- a/include/asterisk/iostream.h
+++ b/include/asterisk/iostream.h
@@ -89,6 +89,11 @@ void ast_iostream_set_timeout_idle_inactivity(struct ast_iostream *stream, int t
* to complete an operation that can take several I/O calls. The
* main use is as an authentication timer with us.
*
+ * \warning To be effective, the socket must be in non-blocking mode because the timeout
+ * can only be checked after a read or write operation returns. If the socket is in blocking
+ * mode (the default), those calls may block for longer than the specified timeout, possibly
+ * much longer.
+ *
* \note Setting timeout to -1 disables the timeout.
* \note Setting this timeout replaces the inactivity timeout timer.
*/
diff --git a/include/asterisk/websocket_client.h b/include/asterisk/websocket_client.h
index 0b12e62ba6..c88449ce51 100644
--- a/include/asterisk/websocket_client.h
+++ b/include/asterisk/websocket_client.h
@@ -52,6 +52,7 @@ enum ast_ws_client_fields {
AST_WS_CLIENT_FIELD_PINGPONGS = (1ULL << 26),
AST_WS_CLIENT_FIELD_PINGPONG_INTERVAL = (1ULL << 27),
AST_WS_CLIENT_FIELD_PINGPONG_PROBES = (1ULL << 28),
+ AST_WS_CLIENT_FIELD_WRITE_TIMEOUT = (1ULL << 29),
AST_WS_CLIENT_NEEDS_RECONNECT = AST_WS_CLIENT_FIELD_URI | AST_WS_CLIENT_FIELD_PROTOCOLS
| AST_WS_CLIENT_FIELD_CONNECTION_TYPE
| AST_WS_CLIENT_FIELD_USERNAME | AST_WS_CLIENT_FIELD_PASSWORD
@@ -65,7 +66,7 @@ enum ast_ws_client_fields {
| AST_WS_CLIENT_FIELD_TCP_KEEPALIVE_TIME | AST_WS_CLIENT_FIELD_TCP_KEEPALIVE_INTERVAL
| AST_WS_CLIENT_FIELD_TCP_KEEPALIVE_PROBES
| AST_WS_CLIENT_FIELD_PINGPONGS | AST_WS_CLIENT_FIELD_PINGPONG_INTERVAL
- | AST_WS_CLIENT_FIELD_PINGPONG_PROBES,
+ | AST_WS_CLIENT_FIELD_PINGPONG_PROBES | AST_WS_CLIENT_FIELD_WRITE_TIMEOUT,
};
/*
@@ -105,6 +106,7 @@ struct ast_websocket_client {
int pingpongs; /*!< Enable WebSocket PING/PONGs */
unsigned int pingpong_interval; /*!< Send WebSocket PINGs at this interval in seconds */
unsigned int pingpong_probes; /*!< Close connection after this many missed PONG responses */
+ int write_timeout; /*!< Write timeout (ms) */
};
/*!
diff --git a/res/ari/ari_doc.xml b/res/ari/ari_doc.xml
index dd7e54cdfa..07544c4653 100644
--- a/res/ari/ari_doc.xml
+++ b/res/ari/ari_doc.xml
@@ -30,12 +30,25 @@
<version>11.11.0</version>
<version>12.4.0</version>
</since>
- <synopsis>The timeout (in milliseconds) to set on WebSocket connections.</synopsis>
+ <synopsis>Write timeout (ms).</synopsis>
<description>
- <para>If a websocket connection accepts input slowly, the timeout
- for writes to it can be increased to keep it from being disconnected.
- Value is in milliseconds.</para>
+ <para>
+ The maximum number of milliseconds to wait for a write
+ to the WebSocket to succeed. The WebSocket will be closed
+ when the timeout is reached. For outgoing WebSocket connections,
+ this sets the default timeout which can be overridden in websocket_client.conf.
+ </para>
+ <note>
+ <para>
+ Success means the write request was accepted by the
+ operating system and does not imply the payload was
+ actually transmitted or received by the server.
+ </para>
+ </note>
</description>
+ <see-also>
+ <ref type="configOption" module="res_websocket_client">write_timeout</ref>
+ </see-also>
</configOption>
<configOption name="pretty">
<since>
diff --git a/res/ari/ari_websockets.c b/res/ari/ari_websockets.c
index 13d998317a..ce21b1e8fa 100644
--- a/res/ari/ari_websockets.c
+++ b/res/ari/ari_websockets.c
@@ -703,17 +703,12 @@ static struct ari_ws_session *session_create(
static int session_update(struct ari_ws_session *ari_ws_session,
struct ast_websocket *ast_ws_session, int send_registered_events)
{
- RAII_VAR(struct ari_conf_general *, general, ari_conf_get_general(), ao2_cleanup);
int i;
if (ast_ws_session == NULL) {
return -1;
}
- if (!general) {
- return -1;
- }
-
ari_ws_session->remote_addr = ast_strdup(ast_sockaddr_stringify(
ast_websocket_remote_address(ast_ws_session)));
if (!ari_ws_session->remote_addr) {
@@ -728,11 +723,6 @@ static int session_update(struct ari_ws_session *ari_ws_session,
return -1;
}
- if (ast_websocket_set_timeout(ast_ws_session, general->write_timeout)) {
- ast_log(LOG_WARNING, "Failed to set write timeout %d on ARI web socket\n",
- general->write_timeout);
- }
-
ast_websocket_ref(ast_ws_session);
ari_ws_session->ast_ws_session = ast_ws_session;
ao2_lock(ari_ws_session);
@@ -818,9 +808,12 @@ static void websocket_established_cb(struct ast_websocket *ast_ws_session,
char *remote_addr = ast_sockaddr_stringify(
ast_websocket_remote_address(ast_ws_session));
const char *session_id = ast_websocket_session_id(ast_ws_session);
-
+ struct ari_conf_general *general = ari_conf_get_general();
+ int general_write_timeout = general ? general->write_timeout : AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT;
SCOPE_ENTER(2, "%s: WebSocket established\n", remote_addr);
+ ao2_cleanup(general);
+
if (TRACE_ATLEAST(2)) {
ast_trace(2, "%s: Websocket Upgrade Headers:\n", remote_addr);
for (v = upgrade_headers; v; v = v->next) {
@@ -844,6 +837,8 @@ static void websocket_established_cb(struct ast_websocket *ast_ws_session,
remote_addr, session_id);
}
+ ast_websocket_set_timeout(ast_ws_session, general_write_timeout);
+
/*
* Since this is a new inbound websocket session,
* session_register_apps() will have already sent "ApplicationRegistered"
@@ -951,7 +946,10 @@ static void *outbound_session_handler_thread(void *obj)
{
struct ari_ws_session *session = obj;
int already_sent_registers = 1;
+ struct ari_conf_general *general = ari_conf_get_general();
+ int general_write_timeout = general ? general->write_timeout : AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT;
+ ao2_cleanup(general);
/*
* We use pthread_cleanup_push because RAII destructors don't run
* if we cancel the thread.
@@ -969,6 +967,7 @@ static void *outbound_session_handler_thread(void *obj)
enum ast_websocket_result result;
struct ast_json *msg;
+
ast_debug(3, "%s: Attempting to connect to %s\n", session->session_id,
session->owc->websocket_client->uri);
@@ -1005,6 +1004,21 @@ static void *outbound_session_handler_thread(void *obj)
session->type == AST_WS_TYPE_CLIENT_PERSISTENT ? session->session_id : session->channel_name,
session->owc->websocket_client->uri);
+ /*
+ * If websocket_client->write_timeout was set in websocket_client.conf, it will
+ * have been applied to the websocket by ast_websocket_client_connect() above.
+ * If it wasn't set in websocket_client.conf, the value will be INT_MAX and
+ * and ast_websocket_client_connect() will have set AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT
+ * on the websocket. However, the user may have set write_timeout in the general section
+ * of ari.conf so if it wasn't set in websocket_client.conf, we'll now set the websocket
+ * timeout to that. If they haven't set it in ari.conf either, it'll default to
+ * AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT as well so the call below will basically
+ * become a no-op.
+ */
+ if (session->owc->websocket_client->write_timeout == INT_MAX) {
+ ast_websocket_set_timeout(astws, general_write_timeout);
+ }
+
/*
* We only want to send "ApplicationRegistered" events in the
* case of a reconnect. The initial connection will have already sent
diff --git a/res/ari/config.c b/res/ari/config.c
index 6575ef7a66..2223da003e 100644
--- a/res/ari/config.c
+++ b/res/ari/config.c
@@ -461,6 +461,11 @@ static int general_apply(const struct ast_sorcery *sorcery, void *obj)
ast_debug(2, "Initializing general config\n");
+ if (general->write_timeout <= 0) {
+ ast_log(LOG_WARNING, "The websocket_write_timeout parameter must be > 0\n");
+ return -1;
+ }
+
parse = ast_strdupa(general->channelvars);
AST_STANDARD_APP_ARGS(args, parse);
@@ -742,7 +747,6 @@ static int ari_conf_init(void)
ast_sorcery_register_int(general, ari_conf_general, websocket_write_timeout, write_timeout,
AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT);
-
ast_sorcery_object_field_register(sorcery, "user", "type", "", OPT_NOOP_T, 0, 0);
ast_sorcery_register_sf(user, ari_conf_user, password, password, "");
ast_sorcery_register_bool(user, ari_conf_user, read_only, read_only, "no");
diff --git a/res/res_http_websocket.c b/res/res_http_websocket.c
index 47e793bce3..9d53ec7acf 100644
--- a/res/res_http_websocket.c
+++ b/res/res_http_websocket.c
@@ -1555,7 +1555,7 @@ static struct ast_websocket * websocket_client_create(
ws->client->version = 13;
ws->opcode = -1;
ws->reconstruct = DEFAULT_RECONSTRUCTION_CEILING;
- ws->timeout = options->timeout;
+ ws->timeout = options->write_timeout;
return ws;
}
@@ -1920,6 +1920,7 @@ struct ast_websocket *AST_OPTIONAL_API_NAME(ast_websocket_client_create)
.protocols = protocols,
.timeout = -1,
.tls_cfg = tls_cfg,
+ .write_timeout = AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT,
};
return ast_websocket_client_create_with_options(&options, result);
diff --git a/res/res_websocket_client.c b/res/res_websocket_client.c
index eff603ee58..9b74f33ec4 100644
--- a/res/res_websocket_client.c
+++ b/res/res_websocket_client.c
@@ -139,6 +139,20 @@ verify_server_hostname = no
<version>22.5.0</version>
</since>
<synopsis>Connection timeout (ms).</synopsis>
+ <description>
+ <para>
+ The maximum number of milliseconds to wait for a connection
+ to the WebSocket server to succeed. If the timer expires,
+ reconnection will be attempted based on the settings of the
+ <literal>reconnect_attempts</literal> and <literal>reconnect_interval</literal>
+ parameters.
+ </para>
+ </description>
+ <see-also>
+ <ref type="configOption">write_timeout</ref>
+ <ref type="configOption">reconnect_attempts</ref>
+ <ref type="configOption">reconnect_interval</ref>
+ </see-also>
</configOption>
<configOption name="reconnect_attempts">
<since>
@@ -156,6 +170,10 @@ verify_server_hostname = no
how often failure messages are logged.
</para>
</description>
+ <see-also>
+ <ref type="configOption">connection_timeout</ref>
+ <ref type="configOption">reconnect_interval</ref>
+ </see-also>
</configOption>
<configOption name="reconnect_interval">
<since>
@@ -164,6 +182,38 @@ verify_server_hostname = no
<version>22.5.0</version>
</since>
<synopsis>How often should reconnection be attempted (ms)?</synopsis>
+ <see-also>
+ <ref type="configOption">connection_timeout</ref>
+ <ref type="configOption">reconnect_attempts</ref>
+ </see-also>
+ </configOption>
+ <configOption name="write_timeout">
+ <since>
+ <version>20.22.0</version>
+ <version>22.12.0</version>
+ <version>23.6.0</version>
+ <version>24.0.0</version>
+ </since>
+ <synopsis>Write timeout (ms).</synopsis>
+ <description>
+ <para>
+ The maximum number of milliseconds to wait for a write
+ to the WebSocket to succeed. If this connection is used
+ by chan_websocket, the WebSocket will be closed and the
+ channel hung up when the timeout is reached. The default
+ value is set by the module creating the client.
+ </para>
+ <note>
+ <para>
+ Success means the write request was accepted by the
+ operating system and does not imply the payload was
+ actually transmitted or received by the server.
+ </para>
+ </note>
+ </description>
+ <see-also>
+ <ref type="configOption">connection_timeout</ref>
+ </see-also>
</configOption>
<configOption name="tls_enabled">
<since>
@@ -397,6 +447,7 @@ struct ast_websocket *ast_websocket_client_connect(struct ast_websocket_client *
.username = wc->username,
.password = wc->password,
.timeout = wc->connect_timeout,
+ .write_timeout = wc->write_timeout != INT_MAX ? wc->write_timeout : AST_DEFAULT_WEBSOCKET_WRITE_TIMEOUT,
.suppress_connection_msgs = 1,
.proxy_host = wc->proxy_host,
.proxy_username = wc->proxy_username,
@@ -612,6 +663,12 @@ static int wc_apply(const struct ast_sorcery *sorcery, void *obj)
res = -1;
}
}
+
+ if (wc->write_timeout <= 0) {
+ ast_log(LOG_WARNING, "The write_timeout parameter must be > 0\n");
+ res = -1;
+ }
+
if (res != 0) {
ast_log(LOG_WARNING, "%s: Websocket client configuration failed\n", id);
} else {
@@ -720,6 +777,8 @@ enum ast_ws_client_fields ast_websocket_client_get_field_diff(
changed |= AST_WS_CLIENT_FIELD_PINGPONG_INTERVAL;
} else if (ast_strings_equal(v->name, "pingpong_probes")) {
changed |= AST_WS_CLIENT_FIELD_PINGPONG_PROBES;
+ } else if (ast_strings_equal(v->name, "write_timeout")) {
+ changed |= AST_WS_CLIENT_FIELD_WRITE_TIMEOUT;
} else {
ast_debug(2, "%s: Unknown change %s\n", new_id, v->name);
}
@@ -803,6 +862,11 @@ static int load_module(void)
ast_sorcery_register_bool(websocket_client, ast_websocket_client, enable_pingpongs, pingpongs, "no");
ast_sorcery_register_uint(websocket_client, ast_websocket_client, pingpong_interval, pingpong_interval, 20);
ast_sorcery_register_uint(websocket_client, ast_websocket_client, pingpong_probes, pingpong_probes, 3);
+ /*
+ * The default for write_timeout needs to be INT_MAX so we can tell that it's not set.
+ * This allows the modules using this capability to apply their own default value.
+ */
+ ast_sorcery_register_int(websocket_client, ast_websocket_client, write_timeout, write_timeout, INT_MAX);
ast_sorcery_load(sorcery);