Welcome! Log In Create A New Profile

Advanced

[PATCH] MINOR: http: custom status reason.

Posted by Robin H. Johnson 
Robin H. Johnson
[PATCH] MINOR: http: custom status reason.
January 01, 2017 10:20PM
The older 'rsprep' directive allows modification of the status reason.

Extend 'http-response set-status' to take an optional string of the new
status reason.

http-response set-status 418 reason "I'm a coffeepot"

Matching updates in Lua code:
- AppletHTTP.set_status
- HTTP.res_set_status

Signed-off-by: Robin H. Johnson <[email protected]>
---
doc/configuration.txt | 9 ++++++---
doc/lua-api/index.rst | 11 +++++++----
include/proto/proto_http.h | 2 +-
include/types/action.h | 1 +
include/types/applet.h | 1 +
src/hlua.c | 14 +++++++++++---
src/proto_http.c | 23 +++++++++++++++++------
tests/setstatus.lua | 26 ++++++++++++++++++++++++++
tests/test-http-set-status-lua.cfg | 31 +++++++++++++++++++++++++++++++
tests/test-http-set-status.cfg | 32 ++++++++++++++++++++++++++++++++
10 files changed, 133 insertions(+), 17 deletions(-)
create mode 100644 tests/setstatus.lua
create mode 100644 tests/test-http-set-status-lua.cfg
create mode 100644 tests/test-http-set-status.cfg

diff --git a/doc/configuration.txt b/doc/configuration.txt
index f24c39623..c0d6ee5ff 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -4122,7 +4122,7 @@ http-response { allow | deny | add-header <name> <fmt> | set-nice <nice> |
set-header <name> <fmt> | del-header <name> |
replace-header <name> <regex-match> <replace-fmt> |
replace-value <name> <regex-match> <replace-fmt> |
- set-status <status> |
+ set-status <status> [reason <str>] |
set-log-level <level> | set-mark <mark> | set-tos <tos> |
add-acl(<file name>) <key fmt> |
del-acl(<file name>) <key fmt> |
@@ -4215,13 +4215,16 @@ http-response { allow | deny | add-header <name> <fmt> | set-nice <nice> |
Cache-Control: max-age=3600, private

- "set-status" replaces the response status code with <status> which must
- be an integer between 100 and 999. Note that the reason is automatically
- adapted to the new code.
+ be an integer between 100 and 999. Optionally, a custom reason text can be
+ provided defined by <str>, or the default reason for the specified code
+ will be used as a fallback.

Example:

# return "431 Request Header Fields Too Large"
http-response set-status 431
+ # return "503 Slow Down", custom reason
+ http-response set-status 503 reason "Slow Down".

- "set-nice" sets the "nice" factor of the current request being processed.
It only has effect against the other requests being processed at the same
diff --git a/doc/lua-api/index.rst b/doc/lua-api/index.rst
index f930277df..e04c5b5a7 100644
--- a/doc/lua-api/index.rst
+++ b/doc/lua-api/index.rst
@@ -1443,13 +1443,15 @@ HTTP class
:param class_http http: The related http object.
:param string uri: The new uri.

-.. js:function:: HTTP.res_set_status(http, status)
+.. js:function:: HTTP.res_set_status(http, status [, reason])

- Rewrites the response status code with the parameter "code". Note that the
- reason is automatically adapted to the new code.
+ Rewrites the response status code with the parameter "code".
+
+ If no custom reason is provided, it will be generated from the status.

:param class_http http: The related http object.
:param integer status: The new response status code.
+ :param string reason: The new response reason (optional).

.. _txn_class:

@@ -2080,13 +2082,14 @@ AppletHTTP class
AppletHTTP.headers["accept"][2] = "*/*, q=0.1"
..

-.. js:function:: AppletHTTP.set_status(applet, code)
+.. js:function:: AppletHTTP.set_status(applet, code [, reason])

This function sets the HTTP status code for the response. The allowed code are
from 100 to 599.

:param class_AppletHTTP applet: An :ref:`applethttp_class`
:param integer code: the status code returned to the client.
+ :param string reason: the status reason returned to the client (optional).

.. js:function:: AppletHTTP.add_header(applet, name, value)

diff --git a/include/proto/proto_http.h b/include/proto/proto_http.h
index 5ee2e2fa4..6c81766aa 100644
--- a/include/proto/proto_http.h
+++ b/include/proto/proto_http.h
@@ -108,7 +108,7 @@ int http_header_match2(const char *hdr, const char *end, const char *name, int l
int http_remove_header2(struct http_msg *msg, struct hdr_idx *idx, struct hdr_ctx *ctx);
int http_header_add_tail2(struct http_msg *msg, struct hdr_idx *hdr_idx, const char *text, int len);
int http_replace_req_line(int action, const char *replace, int len, struct proxy *px, struct stream *s);
-void http_set_status(unsigned int status, struct stream *s);
+void http_set_status(unsigned int status, const char *reason, struct stream *s);
int http_transform_header_str(struct stream* s, struct http_msg *msg, const char* name,
unsigned int name_len, const char *str, struct my_regex *re,
int action);
diff --git a/include/types/action.h b/include/types/action.h
index 5a70db06c..1c1715492 100644
--- a/include/types/action.h
+++ b/include/types/action.h
@@ -135,6 +135,7 @@ struct act_rule {
} cap;
struct {
unsigned int code; /* HTTP status code */
+ const char *reason; /* HTTP status reason */
} status;
struct {
struct sample_expr *expr;
diff --git a/include/types/applet.h b/include/types/applet.h
index 41dc4f250..a2435882e 100644
--- a/include/types/applet.h
+++ b/include/types/applet.h
@@ -138,6 +138,7 @@ struct appctx {
int left_bytes; /* The max amount of bytes that we can read. */
int flags;
int status;
+ const char *reason;
struct task *task;
} hlua_apphttp;
struct {
diff --git a/src/hlua.c b/src/hlua.c
index 09ccdbca2..4e6bf04c1 100644
--- a/src/hlua.c
+++ b/src/hlua.c
@@ -3573,6 +3573,7 @@ static int hlua_applet_http_new(lua_State *L, struct appctx *ctx)
lua_rawseti(L, -2, 0);
appctx->appctx = ctx;
appctx->appctx->ctx.hlua_apphttp.status = 200; /* Default status code returned. */
+ appctx->appctx->ctx.hlua_apphttp.reason = NULL; /* Use default reason based on status */
appctx->htxn.s = s;
appctx->htxn.p = px;

@@ -4089,6 +4090,7 @@ __LJMP static int hlua_applet_http_status(lua_State *L)
{
struct hlua_appctx *appctx = MAY_LJMP(hlua_checkapplet_http(L, 1));
int status = MAY_LJMP(luaL_checkinteger(L, 2));
+ const char *reason = MAY_LJMP(luaL_optlstring(L, 3, NULL, NULL));

if (status < 100 || status > 599) {
lua_pushboolean(L, 0);
@@ -4096,6 +4098,7 @@ __LJMP static int hlua_applet_http_status(lua_State *L)
}

appctx->appctx->ctx.hlua_apphttp.status = status;
+ appctx->appctx->ctx.hlua_apphttp.reason = reason;
lua_pushboolean(L, 1);
return 1;
}
@@ -4148,12 +4151,16 @@ __LJMP static int hlua_applet_http_start_response(lua_State *L)
int hdr_connection = 0;
int hdr_contentlength = -1;
int hdr_chunked = 0;
+ const char *reason = appctx->appctx->ctx.hlua_apphttp.reason;
+
+ if (reason == NULL)
+ reason = get_reason(appctx->appctx->ctx.hlua_apphttp.status);

/* Use the same http version than the request. */
chunk_appendf(tmp, "HTTP/1.%c %d %s\r\n",
appctx->appctx->ctx.hlua_apphttp.flags & APPLET_HTTP11 ? '1' : '0',
appctx->appctx->ctx.hlua_apphttp.status,
- get_reason(appctx->appctx->ctx.hlua_apphttp.status));
+ reason);

/* Get the array associated to the field "response" in the object AppletHTTP. */
lua_pushvalue(L, 0);
@@ -4707,17 +4714,18 @@ static int hlua_http_req_set_uri(lua_State *L)
return 1;
}

-/* This function set the response code. */
+/* This function set the response code & optionally reason. */
static int hlua_http_res_set_status(lua_State *L)
{
struct hlua_txn *htxn = MAY_LJMP(hlua_checkhttp(L, 1));
unsigned int code = MAY_LJMP(luaL_checkinteger(L, 2));
+ const char *reason = MAY_LJMP(luaL_optlstring(L, 3, NULL, NULL));

/* Check if a valid response is parsed */
if (unlikely(htxn->s->txn->rsp.msg_state < HTTP_MSG_BODY))
return 0;

- http_set_status(code, htxn->s);
+ http_set_status(code, reason, htxn->s);
return 0;
}

diff --git a/src/proto_http.c b/src/proto_http.c
index aa8d9973e..228473788 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -12417,14 +12417,14 @@ int http_replace_req_line(int action, const char *replace, int len,
/* This function replace the HTTP status code and the associated message. The
* variable <status> contains the new status code. This function never fails.
*/
-void http_set_status(unsigned int status, struct stream *s)
+void http_set_status(unsigned int status, const char *reason, struct stream *s)
{
struct http_txn *txn = s->txn;
char *cur_ptr, *cur_end;
int delta;
char *res;
int c_l;
- const char *msg;
+ const char *msg = reason;
int msg_len;

chunk_reset(&trash);
@@ -12435,9 +12435,10 @@ void http_set_status(unsigned int status, struct stream *s)
trash.str[c_l] = ' ';
trash.len = c_l + 1;

- msg = get_reason(status);
+ /* Do we have a custom reason format string? */
+ if (msg == NULL)
+ msg = get_reason(status);
msg_len = strlen(msg);
-
strncpy(&trash.str[trash.len], msg, trash.size - trash.len);
trash.len += msg_len;

@@ -12483,7 +12484,7 @@ enum act_return http_action_set_req_line(struct act_rule *rule, struct proxy *px
enum act_return action_http_set_status(struct act_rule *rule, struct proxy *px,
struct session *sess, struct stream *s, int flags)
{
- http_set_status(rule->arg.status.code, s);
+ http_set_status(rule->arg.status.code, rule->arg.status.reason, s);
return ACT_RET_CONT;
}

@@ -12559,7 +12560,7 @@ enum act_parse_ret parse_http_set_status(const char **args, int *orig_arg, struc

/* Check if an argument is available */
if (!*args[*orig_arg]) {
- memprintf(err, "expects exactly 1 argument <status>");
+ memprintf(err, "expects 1 argument: <status>; or 3 arguments: <status> reason <fmt>");
return ACT_RET_PRS_ERR;
}

@@ -12571,6 +12572,16 @@ enum act_parse_ret parse_http_set_status(const char **args, int *orig_arg, struc
}

(*orig_arg)++;
+
+ /* scustom reason string */
+ rule->arg.status.reason = NULL; // If null, we use the default reason for the status code.
+ if (*args[*orig_arg] && strcmp(args[*orig_arg], "reason") == 0 &&
+ (*args[*orig_arg + 1] && strcmp(args[*orig_arg + 1], "if") != 0 && strcmp(args[*orig_arg + 1], "unless") != 0)) {
+ (*orig_arg)++;
+ rule->arg.status.reason = strdup(args[*orig_arg]);
+ (*orig_arg)++;
+ }
+
return ACT_RET_PRS_OK;
}

diff --git a/tests/setstatus.lua b/tests/setstatus.lua
new file mode 100644
index 000000000..e2eafe12f
--- /dev/null
+++ b/tests/setstatus.lua
@@ -0,0 +1,26 @@
+-- http-response actions
+core.register_action("set-status-418-defaultreason", {"http-res"}, function(txn)
+ txn.http:res_set_status(418)
+end)
+core.register_action("set-status-418-customreason", {"http-res"}, function(txn)
+ txn.http:res_set_status(418, "I'm a coffeepot")
+end)
+
+-- http services
+core.register_service("http418-default", "http", function(applet)
+ local response = "Hello World !"
+ applet:set_status(418)
+ applet:add_header("content-length", string.len(response))
+ applet:add_header("content-type", "text/plain")
+ applet:start_response()
+ applet:send(response)
+end)
+
+core.register_service("http418-coffeepot", "http", function(applet)
+ local response = "Hello World !"
+ applet:set_status(418, "I'm a coffeepot")
+ applet:add_header("content-length", string.len(response))
+ applet:add_header("content-type", "text/plain")
+ applet:start_response()
+ applet:send(response)
+end)
diff --git a/tests/test-http-set-status-lua.cfg b/tests/test-http-set-status-lua.cfg
new file mode 100644
index 000000000..485a1ee32
--- /dev/null
+++ b/tests/test-http-set-status-lua.cfg
@@ -0,0 +1,31 @@
+global
+ maxconn 100
+ lua-load setstatus.lua
+
+defaults
+ mode http
+ timeout client 10000
+ timeout server 10000
+ timeout connect 10000
+
+# Expect HTTP/1.1 418 I'm a teapot
+listen lua-service-set-status-defaultreason
+ bind :8003
+ http-request use-service lua.http418-default
+
+# Expect HTTP/1.1 418 I'm a coffeepot
+listen lua-service-set-status-customreason
+ bind :8004
+ http-request use-service lua.http418-coffeepot
+
+# Expect HTTP/1.1 418 I'm a teapot
+listen lua-action-set-status-defaultreason
+ bind :8005
+ http-response lua.set-status-418-defaultreason
+ server host 127.0.0.1:8080
+
+# Expect HTTP/1.1 418 I'm a coffeepot
+listen lua-action-set-status-customreason
+ bind :8006
+ http-response lua.set-status-418-customreason
+ server host 127.0.0.1:8080
diff --git a/tests/test-http-set-status.cfg b/tests/test-http-set-status.cfg
new file mode 100644
index 000000000..0c66b1639
--- /dev/null
+++ b/tests/test-http-set-status.cfg
@@ -0,0 +1,32 @@
+global
+ maxconn 100
+
+defaults
+ mode http
+ timeout client 10000
+ timeout server 10000
+ timeout connect 10000
+
+# Expect HTTP/1.1 418 I'm a teapot
+listen http-response-set-status-defaultreason
+ bind :8001
+ server host 127.0.0.1:8080
+ http-response set-status 418
+
+# Expect HTTP/1.1 418 I'm a coffeepot
+listen http-response-set-status-customreason
+ bind :8002
+ server host 127.0.0.1:8080
+ http-response set-status 418 reason "I'm a coffeepot"
+
+# Expect config parse fail
+#listen parse-fail-string
+# bind :8002
+# server host 127.0.0.1:8080
+# http-response set-status 418 reason
+
+# Expect config parse fail
+#listen parse-fail-keyword
+# bind :8002
+# server host 127.0.0.1:8080
+# http-response set-status 418 "Missing reason keyword"
--
2.11.0.rc2
Willy Tarreau
Re: [PATCH] MINOR: http: custom status reason.
January 02, 2017 11:50AM
On Sun, Jan 01, 2017 at 01:10:52PM -0800, Robin H. Johnson wrote:
> The older 'rsprep' directive allows modification of the status reason.
>
> Extend 'http-response set-status' to take an optional string of the new
> status reason.
>
> http-response set-status 418 reason "I'm a coffeepot"
>
> Matching updates in Lua code:
> - AppletHTTP.set_status
> - HTTP.res_set_status
>
> Signed-off-by: Robin H. Johnson <[email protected]>

Looks good, thanks. I'm CCing Thierry so that he can check if the Lua
part is OK as well. I think it's reasonable to backport this to 1.7.
However for 1.6 I'd rather avoid adding new features, unless participants
here are fine with it being backported. Do you really need it there ?

Willy

> ---
> doc/configuration.txt | 9 ++++++---
> doc/lua-api/index.rst | 11 +++++++----
> include/proto/proto_http.h | 2 +-
> include/types/action.h | 1 +
> include/types/applet.h | 1 +
> src/hlua.c | 14 +++++++++++---
> src/proto_http.c | 23 +++++++++++++++++------
> tests/setstatus.lua | 26 ++++++++++++++++++++++++++
> tests/test-http-set-status-lua.cfg | 31 +++++++++++++++++++++++++++++++
> tests/test-http-set-status.cfg | 32 ++++++++++++++++++++++++++++++++
> 10 files changed, 133 insertions(+), 17 deletions(-)
> create mode 100644 tests/setstatus.lua
> create mode 100644 tests/test-http-set-status-lua.cfg
> create mode 100644 tests/test-http-set-status.cfg
>
> diff --git a/doc/configuration.txt b/doc/configuration.txt
> index f24c39623..c0d6ee5ff 100644
> --- a/doc/configuration.txt
> +++ b/doc/configuration.txt
> @@ -4122,7 +4122,7 @@ http-response { allow | deny | add-header <name> <fmt> | set-nice <nice> |
> set-header <name> <fmt> | del-header <name> |
> replace-header <name> <regex-match> <replace-fmt> |
> replace-value <name> <regex-match> <replace-fmt> |
> - set-status <status> |
> + set-status <status> [reason <str>] |
> set-log-level <level> | set-mark <mark> | set-tos <tos> |
> add-acl(<file name>) <key fmt> |
> del-acl(<file name>) <key fmt> |
> @@ -4215,13 +4215,16 @@ http-response { allow | deny | add-header <name> <fmt> | set-nice <nice> |
> Cache-Control: max-age=3600, private
>
> - "set-status" replaces the response status code with <status> which must
> - be an integer between 100 and 999. Note that the reason is automatically
> - adapted to the new code.
> + be an integer between 100 and 999. Optionally, a custom reason text can be
> + provided defined by <str>, or the default reason for the specified code
> + will be used as a fallback.
>
> Example:
>
> # return "431 Request Header Fields Too Large"
> http-response set-status 431
> + # return "503 Slow Down", custom reason
> + http-response set-status 503 reason "Slow Down".
>
> - "set-nice" sets the "nice" factor of the current request being processed.
> It only has effect against the other requests being processed at the same
> diff --git a/doc/lua-api/index.rst b/doc/lua-api/index.rst
> index f930277df..e04c5b5a7 100644
> --- a/doc/lua-api/index.rst
> +++ b/doc/lua-api/index.rst
> @@ -1443,13 +1443,15 @@ HTTP class
> :param class_http http: The related http object.
> :param string uri: The new uri.
>
> -.. js:function:: HTTP.res_set_status(http, status)
> +.. js:function:: HTTP.res_set_status(http, status [, reason])
>
> - Rewrites the response status code with the parameter "code". Note that the
> - reason is automatically adapted to the new code.
> + Rewrites the response status code with the parameter "code".
> +
> + If no custom reason is provided, it will be generated from the status.
>
> :param class_http http: The related http object.
> :param integer status: The new response status code.
> + :param string reason: The new response reason (optional).
>
> .. _txn_class:
>
> @@ -2080,13 +2082,14 @@ AppletHTTP class
> AppletHTTP.headers["accept"][2] = "*/*, q=0.1"
> ..
>
> -.. js:function:: AppletHTTP.set_status(applet, code)
> +.. js:function:: AppletHTTP.set_status(applet, code [, reason])
>
> This function sets the HTTP status code for the response. The allowed code are
> from 100 to 599.
>
> :param class_AppletHTTP applet: An :ref:`applethttp_class`
> :param integer code: the status code returned to the client.
> + :param string reason: the status reason returned to the client (optional).
>
> .. js:function:: AppletHTTP.add_header(applet, name, value)
>
> diff --git a/include/proto/proto_http.h b/include/proto/proto_http.h
> index 5ee2e2fa4..6c81766aa 100644
> --- a/include/proto/proto_http.h
> +++ b/include/proto/proto_http.h
> @@ -108,7 +108,7 @@ int http_header_match2(const char *hdr, const char *end, const char *name, int l
> int http_remove_header2(struct http_msg *msg, struct hdr_idx *idx, struct hdr_ctx *ctx);
> int http_header_add_tail2(struct http_msg *msg, struct hdr_idx *hdr_idx, const char *text, int len);
> int http_replace_req_line(int action, const char *replace, int len, struct proxy *px, struct stream *s);
> -void http_set_status(unsigned int status, struct stream *s);
> +void http_set_status(unsigned int status, const char *reason, struct stream *s);
> int http_transform_header_str(struct stream* s, struct http_msg *msg, const char* name,
> unsigned int name_len, const char *str, struct my_regex *re,
> int action);
> diff --git a/include/types/action.h b/include/types/action.h
> index 5a70db06c..1c1715492 100644
> --- a/include/types/action.h
> +++ b/include/types/action.h
> @@ -135,6 +135,7 @@ struct act_rule {
> } cap;
> struct {
> unsigned int code; /* HTTP status code */
> + const char *reason; /* HTTP status reason */
> } status;
> struct {
> struct sample_expr *expr;
> diff --git a/include/types/applet.h b/include/types/applet.h
> index 41dc4f250..a2435882e 100644
> --- a/include/types/applet.h
> +++ b/include/types/applet.h
> @@ -138,6 +138,7 @@ struct appctx {
> int left_bytes; /* The max amount of bytes that we can read. */
> int flags;
> int status;
> + const char *reason;
> struct task *task;
> } hlua_apphttp;
> struct {
> diff --git a/src/hlua.c b/src/hlua.c
> index 09ccdbca2..4e6bf04c1 100644
> --- a/src/hlua.c
> +++ b/src/hlua.c
> @@ -3573,6 +3573,7 @@ static int hlua_applet_http_new(lua_State *L, struct appctx *ctx)
> lua_rawseti(L, -2, 0);
> appctx->appctx = ctx;
> appctx->appctx->ctx.hlua_apphttp.status = 200; /* Default status code returned. */
> + appctx->appctx->ctx.hlua_apphttp.reason = NULL; /* Use default reason based on status */
> appctx->htxn.s = s;
> appctx->htxn.p = px;
>
> @@ -4089,6 +4090,7 @@ __LJMP static int hlua_applet_http_status(lua_State *L)
> {
> struct hlua_appctx *appctx = MAY_LJMP(hlua_checkapplet_http(L, 1));
> int status = MAY_LJMP(luaL_checkinteger(L, 2));
> + const char *reason = MAY_LJMP(luaL_optlstring(L, 3, NULL, NULL));
>
> if (status < 100 || status > 599) {
> lua_pushboolean(L, 0);
> @@ -4096,6 +4098,7 @@ __LJMP static int hlua_applet_http_status(lua_State *L)
> }
>
> appctx->appctx->ctx.hlua_apphttp.status = status;
> + appctx->appctx->ctx.hlua_apphttp.reason = reason;
> lua_pushboolean(L, 1);
> return 1;
> }
> @@ -4148,12 +4151,16 @@ __LJMP static int hlua_applet_http_start_response(lua_State *L)
> int hdr_connection = 0;
> int hdr_contentlength = -1;
> int hdr_chunked = 0;
> + const char *reason = appctx->appctx->ctx.hlua_apphttp.reason;
> +
> + if (reason == NULL)
> + reason = get_reason(appctx->appctx->ctx.hlua_apphttp.status);
>
> /* Use the same http version than the request. */
> chunk_appendf(tmp, "HTTP/1.%c %d %s\r\n",
> appctx->appctx->ctx.hlua_apphttp.flags & APPLET_HTTP11 ? '1' : '0',
> appctx->appctx->ctx.hlua_apphttp.status,
> - get_reason(appctx->appctx->ctx.hlua_apphttp.status));
> + reason);
>
> /* Get the array associated to the field "response" in the object AppletHTTP. */
> lua_pushvalue(L, 0);
> @@ -4707,17 +4714,18 @@ static int hlua_http_req_set_uri(lua_State *L)
> return 1;
> }
>
> -/* This function set the response code. */
> +/* This function set the response code & optionally reason. */
> static int hlua_http_res_set_status(lua_State *L)
> {
> struct hlua_txn *htxn = MAY_LJMP(hlua_checkhttp(L, 1));
> unsigned int code = MAY_LJMP(luaL_checkinteger(L, 2));
> + const char *reason = MAY_LJMP(luaL_optlstring(L, 3, NULL, NULL));
>
> /* Check if a valid response is parsed */
> if (unlikely(htxn->s->txn->rsp.msg_state < HTTP_MSG_BODY))
> return 0;
>
> - http_set_status(code, htxn->s);
> + http_set_status(code, reason, htxn->s);
> return 0;
> }
>
> diff --git a/src/proto_http.c b/src/proto_http.c
> index aa8d9973e..228473788 100644
> --- a/src/proto_http.c
> +++ b/src/proto_http.c
> @@ -12417,14 +12417,14 @@ int http_replace_req_line(int action, const char *replace, int len,
> /* This function replace the HTTP status code and the associated message. The
> * variable <status> contains the new status code. This function never fails.
> */
> -void http_set_status(unsigned int status, struct stream *s)
> +void http_set_status(unsigned int status, const char *reason, struct stream *s)
> {
> struct http_txn *txn = s->txn;
> char *cur_ptr, *cur_end;
> int delta;
> char *res;
> int c_l;
> - const char *msg;
> + const char *msg = reason;
> int msg_len;
>
> chunk_reset(&trash);
> @@ -12435,9 +12435,10 @@ void http_set_status(unsigned int status, struct stream *s)
> trash.str[c_l] = ' ';
> trash.len = c_l + 1;
>
> - msg = get_reason(status);
> + /* Do we have a custom reason format string? */
> + if (msg == NULL)
> + msg = get_reason(status);
> msg_len = strlen(msg);
> -
> strncpy(&trash.str[trash.len], msg, trash.size - trash.len);
> trash.len += msg_len;
>
> @@ -12483,7 +12484,7 @@ enum act_return http_action_set_req_line(struct act_rule *rule, struct proxy *px
> enum act_return action_http_set_status(struct act_rule *rule, struct proxy *px,
> struct session *sess, struct stream *s, int flags)
> {
> - http_set_status(rule->arg.status.code, s);
> + http_set_status(rule->arg.status.code, rule->arg.status.reason, s);
> return ACT_RET_CONT;
> }
>
> @@ -12559,7 +12560,7 @@ enum act_parse_ret parse_http_set_status(const char **args, int *orig_arg, struc
>
> /* Check if an argument is available */
> if (!*args[*orig_arg]) {
> - memprintf(err, "expects exactly 1 argument <status>");
> + memprintf(err, "expects 1 argument: <status>; or 3 arguments: <status> reason <fmt>");
> return ACT_RET_PRS_ERR;
> }
>
> @@ -12571,6 +12572,16 @@ enum act_parse_ret parse_http_set_status(const char **args, int *orig_arg, struc
> }
>
> (*orig_arg)++;
> +
> + /* scustom reason string */
> + rule->arg.status.reason = NULL; // If null, we use the default reason for the status code.
> + if (*args[*orig_arg] && strcmp(args[*orig_arg], "reason") == 0 &&
> + (*args[*orig_arg + 1] && strcmp(args[*orig_arg + 1], "if") != 0 && strcmp(args[*orig_arg + 1], "unless") != 0)) {
> + (*orig_arg)++;
> + rule->arg.status.reason = strdup(args[*orig_arg]);
> + (*orig_arg)++;
> + }
> +
> return ACT_RET_PRS_OK;
> }
>
> diff --git a/tests/setstatus.lua b/tests/setstatus.lua
> new file mode 100644
> index 000000000..e2eafe12f
> --- /dev/null
> +++ b/tests/setstatus.lua
> @@ -0,0 +1,26 @@
> +-- http-response actions
> +core.register_action("set-status-418-defaultreason", {"http-res"}, function(txn)
> + txn.http:res_set_status(418)
> +end)
> +core.register_action("set-status-418-customreason", {"http-res"}, function(txn)
> + txn.http:res_set_status(418, "I'm a coffeepot")
> +end)
> +
> +-- http services
> +core.register_service("http418-default", "http", function(applet)
> + local response = "Hello World !"
> + applet:set_status(418)
> + applet:add_header("content-length", string.len(response))
> + applet:add_header("content-type", "text/plain")
> + applet:start_response()
> + applet:send(response)
> +end)
> +
> +core.register_service("http418-coffeepot", "http", function(applet)
> + local response = "Hello World !"
> + applet:set_status(418, "I'm a coffeepot")
> + applet:add_header("content-length", string.len(response))
> + applet:add_header("content-type", "text/plain")
> + applet:start_response()
> + applet:send(response)
> +end)
> diff --git a/tests/test-http-set-status-lua.cfg b/tests/test-http-set-status-lua.cfg
> new file mode 100644
> index 000000000..485a1ee32
> --- /dev/null
> +++ b/tests/test-http-set-status-lua.cfg
> @@ -0,0 +1,31 @@
> +global
> + maxconn 100
> + lua-load setstatus.lua
> +
> +defaults
> + mode http
> + timeout client 10000
> + timeout server 10000
> + timeout connect 10000
> +
> +# Expect HTTP/1.1 418 I'm a teapot
> +listen lua-service-set-status-defaultreason
> + bind :8003
> + http-request use-service lua.http418-default
> +
> +# Expect HTTP/1.1 418 I'm a coffeepot
> +listen lua-service-set-status-customreason
> + bind :8004
> + http-request use-service lua.http418-coffeepot
> +
> +# Expect HTTP/1.1 418 I'm a teapot
> +listen lua-action-set-status-defaultreason
> + bind :8005
> + http-response lua.set-status-418-defaultreason
> + server host 127.0.0.1:8080
> +
> +# Expect HTTP/1.1 418 I'm a coffeepot
> +listen lua-action-set-status-customreason
> + bind :8006
> + http-response lua.set-status-418-customreason
> + server host 127.0.0.1:8080
> diff --git a/tests/test-http-set-status.cfg b/tests/test-http-set-status.cfg
> new file mode 100644
> index 000000000..0c66b1639
> --- /dev/null
> +++ b/tests/test-http-set-status.cfg
> @@ -0,0 +1,32 @@
> +global
> + maxconn 100
> +
> +defaults
> + mode http
> + timeout client 10000
> + timeout server 10000
> + timeout connect 10000
> +
> +# Expect HTTP/1.1 418 I'm a teapot
> +listen http-response-set-status-defaultreason
> + bind :8001
> + server host 127.0.0.1:8080
> + http-response set-status 418
> +
> +# Expect HTTP/1.1 418 I'm a coffeepot
> +listen http-response-set-status-customreason
> + bind :8002
> + server host 127.0.0.1:8080
> + http-response set-status 418 reason "I'm a coffeepot"
> +
> +# Expect config parse fail
> +#listen parse-fail-string
> +# bind :8002
> +# server host 127.0.0.1:8080
> +# http-response set-status 418 reason
> +
> +# Expect config parse fail
> +#listen parse-fail-keyword
> +# bind :8002
> +# server host 127.0.0.1:8080
> +# http-response set-status 418 "Missing reason keyword"
> --
> 2.11.0.rc2
Thierry FOURNIER
Re: [PATCH] MINOR: http: custom status reason.
January 02, 2017 07:10PM
On Mon, 2 Jan 2017 11:47:36 +0100
Willy Tarreau <[email protected]> wrote:

> On Sun, Jan 01, 2017 at 01:10:52PM -0800, Robin H. Johnson wrote:
> > The older 'rsprep' directive allows modification of the status reason.
> >
> > Extend 'http-response set-status' to take an optional string of the new
> > status reason.
> >
> > http-response set-status 418 reason "I'm a coffeepot"
> >
> > Matching updates in Lua code:
> > - AppletHTTP.set_status
> > - HTTP.res_set_status
> >
> > Signed-off-by: Robin H. Johnson <[email protected]>
>
> Looks good, thanks. I'm CCing Thierry so that he can check if the Lua
> part is OK as well. I think it's reasonable to backport this to 1.7.
> However for 1.6 I'd rather avoid adding new features, unless participants
> here are fine with it being backported. Do you really need it there ?


Hi. I agree it looks good. It works fine and it dont change the actual
API. Thanks for the test files !

Thierry


> Willy
>
> > ---
> > doc/configuration.txt | 9 ++++++---
> > doc/lua-api/index.rst | 11 +++++++----
> > include/proto/proto_http.h | 2 +-
> > include/types/action.h | 1 +
> > include/types/applet.h | 1 +
> > src/hlua.c | 14 +++++++++++---
> > src/proto_http.c | 23 +++++++++++++++++------
> > tests/setstatus.lua | 26 ++++++++++++++++++++++++++
> > tests/test-http-set-status-lua.cfg | 31 +++++++++++++++++++++++++++++++
> > tests/test-http-set-status.cfg | 32 ++++++++++++++++++++++++++++++++
> > 10 files changed, 133 insertions(+), 17 deletions(-)
> > create mode 100644 tests/setstatus.lua
> > create mode 100644 tests/test-http-set-status-lua.cfg
> > create mode 100644 tests/test-http-set-status.cfg
> >
> > diff --git a/doc/configuration.txt b/doc/configuration.txt
> > index f24c39623..c0d6ee5ff 100644
> > --- a/doc/configuration.txt
> > +++ b/doc/configuration.txt
> > @@ -4122,7 +4122,7 @@ http-response { allow | deny | add-header <name> <fmt> | set-nice <nice> |
> > set-header <name> <fmt> | del-header <name> |
> > replace-header <name> <regex-match> <replace-fmt> |
> > replace-value <name> <regex-match> <replace-fmt> |
> > - set-status <status> |
> > + set-status <status> [reason <str>] |
> > set-log-level <level> | set-mark <mark> | set-tos <tos> |
> > add-acl(<file name>) <key fmt> |
> > del-acl(<file name>) <key fmt> |
> > @@ -4215,13 +4215,16 @@ http-response { allow | deny | add-header <name> <fmt> | set-nice <nice> |
> > Cache-Control: max-age=3600, private
> >
> > - "set-status" replaces the response status code with <status> which must
> > - be an integer between 100 and 999. Note that the reason is automatically
> > - adapted to the new code.
> > + be an integer between 100 and 999. Optionally, a custom reason text can be
> > + provided defined by <str>, or the default reason for the specified code
> > + will be used as a fallback.
> >
> > Example:
> >
> > # return "431 Request Header Fields Too Large"
> > http-response set-status 431
> > + # return "503 Slow Down", custom reason
> > + http-response set-status 503 reason "Slow Down".
> >
> > - "set-nice" sets the "nice" factor of the current request being processed.
> > It only has effect against the other requests being processed at the same
> > diff --git a/doc/lua-api/index.rst b/doc/lua-api/index.rst
> > index f930277df..e04c5b5a7 100644
> > --- a/doc/lua-api/index.rst
> > +++ b/doc/lua-api/index.rst
> > @@ -1443,13 +1443,15 @@ HTTP class
> > :param class_http http: The related http object.
> > :param string uri: The new uri.
> >
> > -.. js:function:: HTTP.res_set_status(http, status)
> > +.. js:function:: HTTP.res_set_status(http, status [, reason])
> >
> > - Rewrites the response status code with the parameter "code". Note that the
> > - reason is automatically adapted to the new code.
> > + Rewrites the response status code with the parameter "code".
> > +
> > + If no custom reason is provided, it will be generated from the status.
> >
> > :param class_http http: The related http object.
> > :param integer status: The new response status code.
> > + :param string reason: The new response reason (optional).
> >
> > .. _txn_class:
> >
> > @@ -2080,13 +2082,14 @@ AppletHTTP class
> > AppletHTTP.headers["accept"][2] = "*/*, q=0.1"
> > ..
> >
> > -.. js:function:: AppletHTTP.set_status(applet, code)
> > +.. js:function:: AppletHTTP.set_status(applet, code [, reason])
> >
> > This function sets the HTTP status code for the response. The allowed code are
> > from 100 to 599.
> >
> > :param class_AppletHTTP applet: An :ref:`applethttp_class`
> > :param integer code: the status code returned to the client.
> > + :param string reason: the status reason returned to the client (optional).
> >
> > .. js:function:: AppletHTTP.add_header(applet, name, value)
> >
> > diff --git a/include/proto/proto_http.h b/include/proto/proto_http.h
> > index 5ee2e2fa4..6c81766aa 100644
> > --- a/include/proto/proto_http.h
> > +++ b/include/proto/proto_http.h
> > @@ -108,7 +108,7 @@ int http_header_match2(const char *hdr, const char *end, const char *name, int l
> > int http_remove_header2(struct http_msg *msg, struct hdr_idx *idx, struct hdr_ctx *ctx);
> > int http_header_add_tail2(struct http_msg *msg, struct hdr_idx *hdr_idx, const char *text, int len);
> > int http_replace_req_line(int action, const char *replace, int len, struct proxy *px, struct stream *s);
> > -void http_set_status(unsigned int status, struct stream *s);
> > +void http_set_status(unsigned int status, const char *reason, struct stream *s);
> > int http_transform_header_str(struct stream* s, struct http_msg *msg, const char* name,
> > unsigned int name_len, const char *str, struct my_regex *re,
> > int action);
> > diff --git a/include/types/action.h b/include/types/action.h
> > index 5a70db06c..1c1715492 100644
> > --- a/include/types/action.h
> > +++ b/include/types/action.h
> > @@ -135,6 +135,7 @@ struct act_rule {
> > } cap;
> > struct {
> > unsigned int code; /* HTTP status code */
> > + const char *reason; /* HTTP status reason */
> > } status;
> > struct {
> > struct sample_expr *expr;
> > diff --git a/include/types/applet.h b/include/types/applet.h
> > index 41dc4f250..a2435882e 100644
> > --- a/include/types/applet.h
> > +++ b/include/types/applet.h
> > @@ -138,6 +138,7 @@ struct appctx {
> > int left_bytes; /* The max amount of bytes that we can read. */
> > int flags;
> > int status;
> > + const char *reason;
> > struct task *task;
> > } hlua_apphttp;
> > struct {
> > diff --git a/src/hlua.c b/src/hlua.c
> > index 09ccdbca2..4e6bf04c1 100644
> > --- a/src/hlua.c
> > +++ b/src/hlua.c
> > @@ -3573,6 +3573,7 @@ static int hlua_applet_http_new(lua_State *L, struct appctx *ctx)
> > lua_rawseti(L, -2, 0);
> > appctx->appctx = ctx;
> > appctx->appctx->ctx.hlua_apphttp.status = 200; /* Default status code returned. */
> > + appctx->appctx->ctx.hlua_apphttp.reason = NULL; /* Use default reason based on status */
> > appctx->htxn.s = s;
> > appctx->htxn.p = px;
> >
> > @@ -4089,6 +4090,7 @@ __LJMP static int hlua_applet_http_status(lua_State *L)
> > {
> > struct hlua_appctx *appctx = MAY_LJMP(hlua_checkapplet_http(L, 1));
> > int status = MAY_LJMP(luaL_checkinteger(L, 2));
> > + const char *reason = MAY_LJMP(luaL_optlstring(L, 3, NULL, NULL));
> >
> > if (status < 100 || status > 599) {
> > lua_pushboolean(L, 0);
> > @@ -4096,6 +4098,7 @@ __LJMP static int hlua_applet_http_status(lua_State *L)
> > }
> >
> > appctx->appctx->ctx.hlua_apphttp.status = status;
> > + appctx->appctx->ctx.hlua_apphttp.reason = reason;
> > lua_pushboolean(L, 1);
> > return 1;
> > }
> > @@ -4148,12 +4151,16 @@ __LJMP static int hlua_applet_http_start_response(lua_State *L)
> > int hdr_connection = 0;
> > int hdr_contentlength = -1;
> > int hdr_chunked = 0;
> > + const char *reason = appctx->appctx->ctx.hlua_apphttp.reason;
> > +
> > + if (reason == NULL)
> > + reason = get_reason(appctx->appctx->ctx.hlua_apphttp.status);
> >
> > /* Use the same http version than the request. */
> > chunk_appendf(tmp, "HTTP/1.%c %d %s\r\n",
> > appctx->appctx->ctx.hlua_apphttp.flags & APPLET_HTTP11 ? '1' : '0',
> > appctx->appctx->ctx.hlua_apphttp.status,
> > - get_reason(appctx->appctx->ctx.hlua_apphttp.status));
> > + reason);
> >
> > /* Get the array associated to the field "response" in the object AppletHTTP. */
> > lua_pushvalue(L, 0);
> > @@ -4707,17 +4714,18 @@ static int hlua_http_req_set_uri(lua_State *L)
> > return 1;
> > }
> >
> > -/* This function set the response code. */
> > +/* This function set the response code & optionally reason. */
> > static int hlua_http_res_set_status(lua_State *L)
> > {
> > struct hlua_txn *htxn = MAY_LJMP(hlua_checkhttp(L, 1));
> > unsigned int code = MAY_LJMP(luaL_checkinteger(L, 2));
> > + const char *reason = MAY_LJMP(luaL_optlstring(L, 3, NULL, NULL));
> >
> > /* Check if a valid response is parsed */
> > if (unlikely(htxn->s->txn->rsp.msg_state < HTTP_MSG_BODY))
> > return 0;
> >
> > - http_set_status(code, htxn->s);
> > + http_set_status(code, reason, htxn->s);
> > return 0;
> > }
> >
> > diff --git a/src/proto_http.c b/src/proto_http.c
> > index aa8d9973e..228473788 100644
> > --- a/src/proto_http.c
> > +++ b/src/proto_http.c
> > @@ -12417,14 +12417,14 @@ int http_replace_req_line(int action, const char *replace, int len,
> > /* This function replace the HTTP status code and the associated message. The
> > * variable <status> contains the new status code. This function never fails.
> > */
> > -void http_set_status(unsigned int status, struct stream *s)
> > +void http_set_status(unsigned int status, const char *reason, struct stream *s)
> > {
> > struct http_txn *txn = s->txn;
> > char *cur_ptr, *cur_end;
> > int delta;
> > char *res;
> > int c_l;
> > - const char *msg;
> > + const char *msg = reason;
> > int msg_len;
> >
> > chunk_reset(&trash);
> > @@ -12435,9 +12435,10 @@ void http_set_status(unsigned int status, struct stream *s)
> > trash.str[c_l] = ' ';
> > trash.len = c_l + 1;
> >
> > - msg = get_reason(status);
> > + /* Do we have a custom reason format string? */
> > + if (msg == NULL)
> > + msg = get_reason(status);
> > msg_len = strlen(msg);
> > -
> > strncpy(&trash.str[trash.len], msg, trash.size - trash.len);
> > trash.len += msg_len;
> >
> > @@ -12483,7 +12484,7 @@ enum act_return http_action_set_req_line(struct act_rule *rule, struct proxy *px
> > enum act_return action_http_set_status(struct act_rule *rule, struct proxy *px,
> > struct session *sess, struct stream *s, int flags)
> > {
> > - http_set_status(rule->arg.status.code, s);
> > + http_set_status(rule->arg.status.code, rule->arg.status.reason, s);
> > return ACT_RET_CONT;
> > }
> >
> > @@ -12559,7 +12560,7 @@ enum act_parse_ret parse_http_set_status(const char **args, int *orig_arg, struc
> >
> > /* Check if an argument is available */
> > if (!*args[*orig_arg]) {
> > - memprintf(err, "expects exactly 1 argument <status>");
> > + memprintf(err, "expects 1 argument: <status>; or 3 arguments: <status> reason <fmt>");
> > return ACT_RET_PRS_ERR;
> > }
> >
> > @@ -12571,6 +12572,16 @@ enum act_parse_ret parse_http_set_status(const char **args, int *orig_arg, struc
> > }
> >
> > (*orig_arg)++;
> > +
> > + /* scustom reason string */
> > + rule->arg.status.reason = NULL; // If null, we use the default reason for the status code.
> > + if (*args[*orig_arg] && strcmp(args[*orig_arg], "reason") == 0 &&
> > + (*args[*orig_arg + 1] && strcmp(args[*orig_arg + 1], "if") != 0 && strcmp(args[*orig_arg + 1], "unless") != 0)) {
> > + (*orig_arg)++;
> > + rule->arg.status.reason = strdup(args[*orig_arg]);
> > + (*orig_arg)++;
> > + }
> > +
> > return ACT_RET_PRS_OK;
> > }
> >
> > diff --git a/tests/setstatus.lua b/tests/setstatus.lua
> > new file mode 100644
> > index 000000000..e2eafe12f
> > --- /dev/null
> > +++ b/tests/setstatus.lua
> > @@ -0,0 +1,26 @@
> > +-- http-response actions
> > +core.register_action("set-status-418-defaultreason", {"http-res"}, function(txn)
> > + txn.http:res_set_status(418)
> > +end)
> > +core.register_action("set-status-418-customreason", {"http-res"}, function(txn)
> > + txn.http:res_set_status(418, "I'm a coffeepot")
> > +end)
> > +
> > +-- http services
> > +core.register_service("http418-default", "http", function(applet)
> > + local response = "Hello World !"
> > + applet:set_status(418)
> > + applet:add_header("content-length", string.len(response))
> > + applet:add_header("content-type", "text/plain")
> > + applet:start_response()
> > + applet:send(response)
> > +end)
> > +
> > +core.register_service("http418-coffeepot", "http", function(applet)
> > + local response = "Hello World !"
> > + applet:set_status(418, "I'm a coffeepot")
> > + applet:add_header("content-length", string.len(response))
> > + applet:add_header("content-type", "text/plain")
> > + applet:start_response()
> > + applet:send(response)
> > +end)
> > diff --git a/tests/test-http-set-status-lua.cfg b/tests/test-http-set-status-lua.cfg
> > new file mode 100644
> > index 000000000..485a1ee32
> > --- /dev/null
> > +++ b/tests/test-http-set-status-lua.cfg
> > @@ -0,0 +1,31 @@
> > +global
> > + maxconn 100
> > + lua-load setstatus.lua
> > +
> > +defaults
> > + mode http
> > + timeout client 10000
> > + timeout server 10000
> > + timeout connect 10000
> > +
> > +# Expect HTTP/1.1 418 I'm a teapot
> > +listen lua-service-set-status-defaultreason
> > + bind :8003
> > + http-request use-service lua.http418-default
> > +
> > +# Expect HTTP/1.1 418 I'm a coffeepot
> > +listen lua-service-set-status-customreason
> > + bind :8004
> > + http-request use-service lua.http418-coffeepot
> > +
> > +# Expect HTTP/1.1 418 I'm a teapot
> > +listen lua-action-set-status-defaultreason
> > + bind :8005
> > + http-response lua.set-status-418-defaultreason
> > + server host 127.0.0.1:8080
> > +
> > +# Expect HTTP/1.1 418 I'm a coffeepot
> > +listen lua-action-set-status-customreason
> > + bind :8006
> > + http-response lua.set-status-418-customreason
> > + server host 127.0.0.1:8080
> > diff --git a/tests/test-http-set-status.cfg b/tests/test-http-set-status.cfg
> > new file mode 100644
> > index 000000000..0c66b1639
> > --- /dev/null
> > +++ b/tests/test-http-set-status.cfg
> > @@ -0,0 +1,32 @@
> > +global
> > + maxconn 100
> > +
> > +defaults
> > + mode http
> > + timeout client 10000
> > + timeout server 10000
> > + timeout connect 10000
> > +
> > +# Expect HTTP/1.1 418 I'm a teapot
> > +listen http-response-set-status-defaultreason
> > + bind :8001
> > + server host 127.0.0.1:8080
> > + http-response set-status 418
> > +
> > +# Expect HTTP/1.1 418 I'm a coffeepot
> > +listen http-response-set-status-customreason
> > + bind :8002
> > + server host 127.0.0.1:8080
> > + http-response set-status 418 reason "I'm a coffeepot"
> > +
> > +# Expect config parse fail
> > +#listen parse-fail-string
> > +# bind :8002
> > +# server host 127.0.0.1:8080
> > +# http-response set-status 418 reason
> > +
> > +# Expect config parse fail
> > +#listen parse-fail-keyword
> > +# bind :8002
> > +# server host 127.0.0.1:8080
> > +# http-response set-status 418 "Missing reason keyword"
> > --
> > 2.11.0.rc2
Robin H. Johnson
Re: [PATCH] MINOR: http: custom status reason.
January 02, 2017 07:20PM
On Mon, Jan 02, 2017 at 11:47:36AM +0100, Willy Tarreau wrote:
> On Sun, Jan 01, 2017 at 01:10:52PM -0800, Robin H. Johnson wrote:
> > The older 'rsprep' directive allows modification of the status reason.
> >
> > Extend 'http-response set-status' to take an optional string of the new
> > status reason.
> >
> > http-response set-status 418 reason "I'm a coffeepot"
> >
> > Matching updates in Lua code:
> > - AppletHTTP.set_status
> > - HTTP.res_set_status
> >
> > Signed-off-by: Robin H. Johnson <[email protected]>
>
> Looks good, thanks. I'm CCing Thierry so that he can check if the Lua
> part is OK as well. I think it's reasonable to backport this to 1.7.
> However for 1.6 I'd rather avoid adding new features, unless participants
> here are fine with it being backported. Do you really need it there ?
1.7 is a little too new, and I was aiming at configuration compatibility
between the releases, so ported it back to the actively maintained
series.

It wasn't clear how to use those tests fully, but Lua set_status does
work for me.

--
Robin Hugh Johnson
Gentoo Linux: Dev, Infra Lead, Foundation Trustee & Treasurer
E-Mail : robbat2@gentoo.org
GnuPG FP : 11ACBA4F 4778E3F6 E4EDF38E B27B944E 34884E85
GnuPG FP : 7D0B3CEB E9B85B1F 825BCECF EE05E6F6 A48F6136
Willy Tarreau
Re: [PATCH] MINOR: http: custom status reason.
January 06, 2017 12:10PM
On Mon, Jan 02, 2017 at 07:00:37PM +0100, Thierry FOURNIER wrote:
> Hi. I agree it looks good. It works fine and it dont change the actual
> API. Thanks for the test files !

Now applied, thanks to you both :-)

Willy
Sorry, only registered users may post in this forum.

Click here to login