REST API response HTTP headers modification. (#28)

* Add support for HTTP response headers modification.

* Add tests for responses with headers.
This commit is contained in:
Eugene Kabanov 2022-06-20 02:04:28 +03:00 committed by GitHub
parent 4609bec638
commit 6e36cbc474
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 320 additions and 70 deletions

View File

@ -29,6 +29,7 @@ type
RestApiResponse* = object
status*: HttpCode
headers*: HttpTable
case kind*: RestApiResponseKind
of RestApiResponseKind.Empty:
discard
@ -55,17 +56,43 @@ type
status*: int
contentType*: string
message*: string
RestKeyValueTuple* = tuple[key: string, value: string]
proc error*(t: typedesc[RestApiResponse],
status: HttpCode = Http200, msg: string = "",
contentType: string = "text/html",
headers: HttpTable): RestApiResponse =
## Create REST API error response with status ``status`` and content specified
## by type ``contentType`` and data ``data``. You can also specify
## additional HTTP response headers using ``headers`` argument.
##
## Please note that ``contentType`` argument's value has priority over
## ``Content-Type`` header's value in ``headers`` table.
RestApiResponse(kind: RestApiResponseKind.Error, status: status,
headers: headers,
errobj: RestApiError(status: status, message: msg,
contentType: contentType))
proc error*(t: typedesc[RestApiResponse],
status: HttpCode = Http200, msg: string = "",
contentType: string = "text/html",
headers: openArray[RestKeyValueTuple]): RestApiResponse =
error(t, status, msg, contentType, HttpTable.init(headers))
proc error*(t: typedesc[RestApiResponse],
status: HttpCode = Http200, msg: string = "",
contentType: string = "text/html"): RestApiResponse =
RestApiResponse(kind: RestApiResponseKind.Error, status: status,
errobj: RestApiError(status: status, message: msg,
contentType: contentType))
error(t, status, msg, contentType, HttpTable.init())
proc response*(t: typedesc[RestApiResponse], data: ByteChar,
status: HttpCode = Http200,
contentType = "text/text"): RestApiResponse =
status: HttpCode = Http200, contentType = "text/text",
headers: HttpTable): RestApiResponse =
## Create REST API data response with status ``status`` and content specified
## by type ``contentType`` and data ``data``. You can also specify
## additional HTTP response headers using ``headers`` argument.
##
## Please note that ``contentType`` argument's value has priority over
## ``Content-Type`` header's value in ``headers`` table.
let content =
when data is seq[byte]:
ContentBody(contentType: contentType, data: data)
@ -74,12 +101,45 @@ proc response*(t: typedesc[RestApiResponse], data: ByteChar,
var default: seq[byte]
ContentBody(contentType: contentType,
data: if len(data) > 0: toBytes(data) else: default)
RestApiResponse(kind: RestApiResponseKind.Content, status: status,
headers: headers, content: content)
RestApiResponse(kind: RestApiResponseKind.Content,
status: status,
content: content)
proc response*(t: typedesc[RestApiResponse], data: ByteChar,
status: HttpCode = Http200, contentType = "text/text",
headers: openArray[RestKeyValueTuple]): RestApiResponse =
response(t, data, status, contentType, HttpTable.init(headers))
proc response*(t: typedesc[RestApiResponse], data: ByteChar,
status: HttpCode = Http200,
contentType = "text/text"): RestApiResponse =
response(t, data, status, contentType, HttpTable.init())
proc redirect*(t: typedesc[RestApiResponse], status: HttpCode = Http307,
location: string, preserveQuery = false): RestApiResponse =
location: string, preserveQuery = false,
headers: HttpTable): RestApiResponse =
## Create REST API redirect response with status ``status`` and new location
## ``location``.
##
## You can preserve HTTP query string `uri.query` part using ``preserveQuery``
## argument. When ``preserveQuery`` is true new query string will be formed as
## concatenation of original HTTP request query string and ``location`` query
## string.
##
## You can also specify additional HTTP response headers using ``headers``
## argument.
##
## Please note that ``location`` argument's value has priority over
## ``Location`` header's value in ``headers`` table.
RestApiResponse(kind: RestApiResponseKind.Redirect, status: status,
location: location, preserveQuery: preserveQuery)
headers: headers, location: location,
preserveQuery: preserveQuery)
proc redirect*(t: typedesc[RestApiResponse], status: HttpCode = Http307,
location: string, preserveQuery = false,
headers: openArray[RestKeyValueTuple]): RestApiResponse =
redirect(t, status, location, preserveQuery, HttpTable.init(headers))
proc redirect*(t: typedesc[RestApiResponse], status: HttpCode = Http307,
location: string,
preserveQuery = false): RestApiResponse =
redirect(t, status, location, preserveQuery, HttpTable.init())

View File

@ -38,6 +38,14 @@ proc originsMatch(requestOrigin, allowedOrigin: string): bool =
else:
false
proc mergeHttpHeaders(a: var HttpTable, b: HttpTable) =
# Copy headers from table ``b`` to table ``a`` whose keys are not present in
# ``a``.
for key, items in b.items():
if key notin a:
for item in items:
a.add(key, item)
proc processRestRequest*[T](server: T,
rf: RequestFence): Future[HttpResponseRef] {.
gcsafe, async.} =
@ -133,6 +141,7 @@ proc processRestRequest*[T](server: T,
content_type = restRes.content.contentType,
content_size = len(restRes.content.data)
headers.mergeHttpHeaders(restRes.headers)
return await request.respond(restRes.status,
restRes.content.data, headers)
of RestApiResponseKind.Error:
@ -141,8 +150,10 @@ proc processRestRequest*[T](server: T,
status = restRes.status.toInt(),
meth = $request.meth, peer = $request.remoteAddress(),
uri = $request.uri, error
let headers = HttpTable.init([("Content-Type",
var headers = HttpTable.init([("Content-Type",
error.contentType)])
headers.mergeHttpHeaders(restRes.headers)
return await request.respond(error.status, error.message,
headers)
of RestApiResponseKind.Redirect:
@ -159,7 +170,8 @@ proc processRestRequest*[T](server: T,
else:
uri.query = uri.query & "&" & request.uri.query
$uri
return await request.redirect(restRes.status, location)
return await request.redirect(restRes.status, location,
restRes.headers)
else:
debug "Response was sent in request handler", meth = $request.meth,
peer = $request.remoteAddress(), uri = $request.uri,

View File

@ -1,4 +1,4 @@
import std/[unittest, strutils]
import std/[unittest, strutils, algorithm]
import helpers
import chronos, chronos/apps
import stew/byteutils
@ -10,6 +10,38 @@ type
ClientResponse = object
status*: int
data*: string
headers*: HttpTable
proc cmpNoHeaders(a, b: ClientResponse): bool =
(a.status == b.status) and (a.data == b.data)
proc cmpWithHeaders(a, b: ClientResponse): bool =
if (a.status != b.status) or (a.data != b.data):
return false
for header in b.headers.items():
if header.key notin a.headers:
return false
let checkItems = a.headers.getList(header.key).sorted()
let expectItems = header.value.sorted()
if checkItems != expectItems:
return false
true
proc init(t: typedesc[ClientResponse], status: int): ClientResponse =
ClientResponse(status: status)
proc init(t: typedesc[ClientResponse], status: int,
data: string): ClientResponse =
ClientResponse(status: status, data: data)
proc init(t: typedesc[ClientResponse], status: int, data: string,
headers: HttpTable): ClientResponse =
ClientResponse(status: status, data: data, headers: headers)
proc init(t: typedesc[ClientResponse], status: int, data: string,
headers: openArray[tuple[key, value: string]]): ClientResponse =
let table = HttpTable.init(headers)
ClientResponse(status: status, data: data, headers: table)
proc httpClient(server: TransportAddress, meth: HttpMethod, url: string,
body: string, ctype = "",
@ -34,15 +66,23 @@ proc httpClient(server: TransportAddress, meth: HttpMethod, url: string,
headersBuf.setLen(rlen)
let resp = parseResponse(headersBuf, true)
doAssert(resp.success())
let headers =
block:
var res = HttpTable.init()
for key, value in resp.headers(headersBuf):
res.add(key, value)
res
let length = resp.contentLength()
doAssert(length >= 0)
let cresp =
if length > 0:
var dataBuf = newString(length)
await transp.readExactly(addr dataBuf[0], len(dataBuf))
ClientResponse(status: resp.code, data: dataBuf)
ClientResponse.init(resp.code, dataBuf, headers)
else:
ClientResponse(status: resp.code, data: "")
ClientResponse.init(resp.code, "", headers)
await transp.closeWait()
return cresp
@ -94,12 +134,12 @@ suite "REST API server test suite":
"//////////////////////////////////////////" &
"//////////////////////////test", "")
check:
res1 == ClientResponse(status: 410)
res2 == ClientResponse(status: 200, data: "ok-1")
res3.status == 505
res4 == ClientResponse(status: 503)
res5 == ClientResponse(status: 404)
res6 == ClientResponse(status: 400)
cmpNoHeaders(res1, ClientResponse.init(410))
cmpNoHeaders(res2, ClientResponse.init(200, "ok-1"))
cmpNoHeaders(res3, ClientResponse.init(505, "Some error"))
cmpNoHeaders(res4, ClientResponse.init(503))
cmpNoHeaders(res5, ClientResponse.init(404))
cmpNoHeaders(res6, ClientResponse.init(400))
finally:
await server.closeWait()
@ -132,35 +172,35 @@ suite "REST API server test suite":
toHex(smp3.get()))
const TestVectors = [
("/test/1234", ClientResponse(status: 200, data: "1234")),
("/test/12345678", ClientResponse(status: 200, data: "12345678")),
("/test/00000001", ClientResponse(status: 200, data: "1")),
("/test/0000000", ClientResponse(status: 200, data: "0")),
("/test/99999999999999999999999", ClientResponse(status: 411)),
("/test/nondec", ClientResponse(status: 404)),
("/test/1234", ClientResponse.init(200, "1234")),
("/test/12345678", ClientResponse.init(200, "12345678")),
("/test/00000001", ClientResponse.init(200, "1")),
("/test/0000000", ClientResponse.init(200, "0")),
("/test/99999999999999999999999", ClientResponse.init(411)),
("/test/nondec", ClientResponse.init(404)),
("/test/1234/text1", ClientResponse(status: 200, data: "1234:text1")),
("/test/1234/text1", ClientResponse.init(200, "1234:text1")),
("/test/12345678/texttext2",
ClientResponse(status: 200, data: "12345678:texttext2")),
ClientResponse.init(200, "12345678:texttext2")),
("/test/00000001/texttexttext3",
ClientResponse(status: 200, data: "1:texttexttext3")),
ClientResponse.init(200, "1:texttexttext3")),
("/test/0000000/texttexttexttext4",
ClientResponse(status: 200, data: "0:texttexttexttext4")),
("/test/nondec/texttexttexttexttext5", ClientResponse(status: 404)),
ClientResponse.init(200, "0:texttexttexttext4")),
("/test/nondec/texttexttexttexttext5", ClientResponse.init(404)),
("/test/99999999999999999999999/texttexttexttexttext5",
ClientResponse(status: 411)),
ClientResponse.init(411)),
("/test/1234/text1/0xCAFE",
ClientResponse(status: 200, data: "1234:text1:cafe")),
ClientResponse.init(200, "1234:text1:cafe")),
("/test/12345678/text2text2/0xdeadbeaf",
ClientResponse(status: 200, data: "12345678:text2text2:deadbeaf")),
ClientResponse.init(200, "12345678:text2text2:deadbeaf")),
("/test/00000001/text3text3text3/0xabcdef012345",
ClientResponse(status: 200, data: "1:text3text3text3:abcdef012345")),
ClientResponse.init(200, "1:text3text3text3:abcdef012345")),
("/test/00000000/text4text4text4text4/0xaa",
ClientResponse(status: 200, data: "0:text4text4text4text4:aa")),
("/test/nondec/text5/0xbb", ClientResponse(status: 404)),
("/test/99999999999999999999999/text6/0xcc", ClientResponse(status: 411)),
("/test/1234/text7/0xxx", ClientResponse(status: 413))
ClientResponse.init(200, "0:text4text4text4text4:aa")),
("/test/nondec/text5/0xbb", ClientResponse.init(404)),
("/test/99999999999999999999999/text6/0xcc", ClientResponse.init(411)),
("/test/1234/text7/0xxx", ClientResponse.init(413))
]
var sres = RestServerRef.new(router, serverAddress)
@ -243,25 +283,25 @@ suite "REST API server test suite":
const TestVectors = [
("/test/1/2/0xaa?opt1=1&opt2=2&opt3=0xbb&opt4=2&opt4=3&opt4=4&opt5=t&" &
"opt5=e&opt5=s&opt5=t&opt6=0xCA&opt6=0xFE",
ClientResponse(status: 200, data: "1:2:aa:1:2:bb:2,3,4:t,e,s,t:ca,fe")),
ClientResponse.init(200, "1:2:aa:1:2:bb:2,3,4:t,e,s,t:ca,fe")),
# Optional argument will not pass decoding procedure `opt1=a`.
("/test/1/2/0xaa?opt1=a&opt2=2&opt3=0xbb&opt4=2&opt4=3&opt4=4&opt5=t&" &
"opt5=e&opt5=s&opt5=t&opt6=0xCA&opt6=0xFE",
ClientResponse(status: 414)),
ClientResponse.init(414)),
# Sequence argument will not pass decoding procedure `opt4=a`.
("/test/1/2/0xaa?opt1=1&opt2=2&opt3=0xbb&opt4=2&opt4=3&opt4=a&opt5=t&" &
"opt5=e&opt5=s&opt5=t&opt6=0xCA&opt6=0xFE",
ClientResponse(status: 417)),
ClientResponse.init(417)),
# Optional argument will not pass decoding procedure `opt3=0xxx`.
("/test/1/2/0xaa?opt1=1&opt2=2&opt3=0xxx&opt4=2&opt4=3&opt4=4&opt5=t&" &
"opt5=e&opt5=s&opt5=t&opt6=0xCA&opt6=0xFE",
ClientResponse(status: 416)),
ClientResponse.init(416)),
# Sequence argument will not pass decoding procedure `opt6=0xxx`.
("/test/1/2/0xaa?opt1=1&opt2=2&opt3=0xbb&opt4=2&opt4=3&opt4=5&opt5=t&" &
"opt5=e&opt5=s&opt5=t&opt6=0xCA&opt6=0xxx",
ClientResponse(status: 421)),
ClientResponse.init(421)),
# All optional arguments are missing
("/test/1/2/0xaa", ClientResponse(status: 200, data: "1:2:aa::::::"))
("/test/1/2/0xaa", ClientResponse.init(200, "1:2:aa::::::"))
]
var sres = RestServerRef.new(router, serverAddress)
@ -354,23 +394,21 @@ suite "REST API server test suite":
const PostVectors = [
(
("/test/1/2/0xaa", "text/text", "textbody"),
ClientResponse(status: 200,
data: "1:2:aa:::::::text/text,textbody")
ClientResponse.init(200, "1:2:aa:::::::text/text,textbody")
),
(
("/test/1/2/0xaa", "", ""),
ClientResponse(status: 400)
ClientResponse.init(400)
),
(
("/test/1/2/0xaa", "text/text", ""),
ClientResponse(status: 200,
data: "1:2:aa:::::::text/text,")
ClientResponse.init(200, "1:2:aa:::::::text/text,")
),
(
("/test/1/2/0xaa?opt1=1&opt2=2&opt3=0xbb&opt4=2&opt4=3&opt4=4&opt5=t&" &
"opt5=e&opt5=s&opt5=t&opt6=0xCA&opt6=0xFE", "text/text", "textbody"),
ClientResponse(status: 200, data:
"1:2:aa:1:2:bb:2,3,4:t,e,s,t:ca,fe:text/text,textbody")
ClientResponse.init(200,
"1:2:aa:1:2:bb:2,3,4:t,e,s,t:ca,fe:text/text,textbody")
)
]
@ -473,22 +511,22 @@ suite "REST API server test suite":
# Empty result with response sent via `resp`.
("/test/1?opt1=2345&opt4=3456&opt4=4567&opt4=5678&opt4=6789",
"text/text", "somebody"),
ClientResponse(status: 200,
data: "1:2345:3456,4567,5678,6789:text/text,somebody")
ClientResponse.init(200,
"1:2345:3456,4567,5678,6789:text/text,somebody")
),
(
# Result with response sent via `resp`.
("/test/2?opt1=2345&opt4=3456&opt4=4567&opt4=5678&opt4=6789",
"text/text", "somebody"),
ClientResponse(status: 200,
data: "2:2345:3456,4567,5678,6789:text/text,somebody")
ClientResponse.init(200,
"2:2345:3456,4567,5678,6789:text/text,somebody")
),
(
# Error with response sent via `resp`.
("/test/3?opt1=2345&opt4=3456&opt4=4567&opt4=5678&opt4=6789",
"text/text", "somebody"),
ClientResponse(status: 200,
data: "3:2345:3456,4567,5678,6789:text/text,somebody")
ClientResponse.init(200,
"3:2345:3456,4567,5678,6789:text/text,somebody")
)
]
@ -496,17 +534,17 @@ suite "REST API server test suite":
(
# Empty result with response sent via `resp`.
"/test/1?opt1=2345&opt4=3456&opt4=4567&opt4=5678&opt4=6789",
ClientResponse(status: 200, data: "1:2345:3456,4567,5678,6789")
ClientResponse.init(200, "1:2345:3456,4567,5678,6789")
),
(
# Result with response sent via `resp`.
"/test/2?opt1=2345&opt4=3456&opt4=4567&opt4=5678&opt4=6789",
ClientResponse(status: 200, data: "2:2345:3456,4567,5678,6789")
ClientResponse.init(200, "2:2345:3456,4567,5678,6789")
),
(
# Error with response sent via `resp`.
"/test/3?opt1=2345&opt4=3456&opt4=4567&opt4=5678&opt4=6789",
ClientResponse(status: 200, data: "3:2345:3456,4567,5678,6789")
ClientResponse.init(200, "3:2345:3456,4567,5678,6789")
)
]
@ -531,42 +569,182 @@ suite "REST API server test suite":
finally:
await server.closeWait()
asyncTest "Responses with headers test":
var router = RestRouter.init(testValidate)
router.api(MethodGet, "/test/get/success") do (
param: Option[string]) -> RestApiResponse:
let test = param.get().get()
case test
of "test1":
let headers = [
("test-header", "SUCCESS"), ("test-header", "TEST"),
("test-header", "1")
]
return RestApiResponse.response("TEST1:OK", Http200, headers = headers)
of "test2":
let headers = HttpTable.init([
("test-header", "SUCCESS"), ("test-header", "TEST"),
("test-header", "2")
])
return RestApiResponse.response("TEST2:OK", Http200, headers = headers)
of "test3":
let headers = HttpTable.init([
("test-header", "SUCCESS"), ("test-header", "TEST"),
("test-header", "3"), ("content-type", "application/success")
])
return RestApiResponse.response("TEST3:OK", Http200,
contentType = "text/success",
headers = headers)
else:
return RestApiResponse.error(Http400)
router.api(MethodGet, "/test/get/error") do (
param: Option[string]) -> RestApiResponse:
let testName = param.get().get()
case testName
of "test1":
let headers = [
("test-header", "ERROR"), ("test-header", "TEST"),
("test-header", "1")
]
return RestApiResponse.error(Http404, "ERROR1:OK", headers = headers)
of "test2":
let headers = HttpTable.init([
("test-header", "ERROR"), ("test-header", "TEST"),
("test-header", "2")
])
return RestApiResponse.error(Http404, "ERROR2:OK", headers = headers)
of "test3":
let headers = HttpTable.init([
("test-header", "ERROR"), ("test-header", "TEST"),
("test-header", "3"), ("content-type", "application/error")
])
return RestApiResponse.error(Http404, "ERROR3:OK",
contentType = "text/error",
headers = headers)
else:
return RestApiResponse.error(Http400)
router.api(MethodGet, "/test/get/redirect") do (
param: Option[string]) -> RestApiResponse:
let testName = param.get().get()
case testName
of "test1":
let headers = [
("test-header", "REDIRECT"), ("test-header", "TEST"),
("test-header", "1")
]
return RestApiResponse.redirect(Http307, "/test/get/redirect1",
preserveQuery = true, headers = headers)
of "test2":
let headers = HttpTable.init([
("test-header", "REDIRECT"), ("test-header", "TEST"),
("test-header", "2")
])
return RestApiResponse.redirect(Http307, "/test/get/redirect2",
preserveQuery = false,
headers = headers)
of "test3":
let headers = HttpTable.init([
("test-header", "REDIRECT"), ("test-header", "TEST"),
("test-header", "3"), ("location", "/test/get/wrong_redirect")
])
return RestApiResponse.redirect(Http307, "/test/get/redirect3",
preserveQuery = true,
headers = headers)
else:
return RestApiResponse.error(Http400)
const HttpHeadersVectors = [
("/test/get/success?param=test1",
ClientResponse.init(200, "TEST1:OK",
[("test-header", "SUCCESS"), ("test-header", "TEST"),
("test-header", "1")])),
("/test/get/success?param=test2",
ClientResponse.init(200, "TEST2:OK",
[("test-header", "SUCCESS"), ("test-header", "TEST"),
("test-header", "2")])),
("/test/get/success?param=test3",
ClientResponse.init(200, "TEST3:OK",
[("test-header", "SUCCESS"), ("test-header", "TEST"),
("test-header", "3"), ("content-type", "text/success")])),
("/test/get/error?param=test1",
ClientResponse.init(404, "ERROR1:OK",
[("test-header", "ERROR"), ("test-header", "TEST"),
("test-header", "1")])),
("/test/get/error?param=test2",
ClientResponse.init(404, "ERROR2:OK",
[("test-header", "ERROR"), ("test-header", "TEST"),
("test-header", "2")])),
("/test/get/error?param=test3",
ClientResponse.init(404, "ERROR3:OK",
[("test-header", "ERROR"), ("test-header", "TEST"),
("test-header", "3"), ("content-type", "text/error")])),
("/test/get/redirect?param=test1",
ClientResponse.init(307, "",
[("test-header", "REDIRECT"), ("test-header", "TEST"),
("test-header", "1"),
("location", "/test/get/redirect1?param=test1")])),
("/test/get/redirect?param=test2",
ClientResponse.init(307, "",
[("test-header", "REDIRECT"), ("test-header", "TEST"),
("test-header", "2"),
("location", "/test/get/redirect2")])),
("/test/get/redirect?param=test3",
ClientResponse.init(307, "",
[("test-header", "REDIRECT"), ("test-header", "TEST"),
("test-header", "3"),
("location", "/test/get/redirect3?param=test3")])),
]
var sres = RestServerRef.new(router, serverAddress)
let server = sres.get()
server.start()
try:
for item in HttpHeadersVectors:
let res = await httpClient(serverAddress, MethodGet, item[0], "")
check cmpWithHeaders(res, item[1])
finally:
await server.closeWait()
asyncTest "preferredContentType() test":
const PostVectors = [
(
("/test/post", "somebody0908", "text/html",
"app/type1;q=0.9,app/type2;q=0.8"),
ClientResponse(status: 200, data: "type1[text/html,somebody0908]")
ClientResponse.init(200, "type1[text/html,somebody0908]")
),
(
("/test/post", "somebody0908", "text/html",
"app/type2;q=0.8,app/type1;q=0.9"),
ClientResponse(status: 200, data: "type1[text/html,somebody0908]")
ClientResponse.init(200, "type1[text/html,somebody0908]")
),
(
("/test/post", "somebody09", "text/html",
"app/type2,app/type1;q=0.9"),
ClientResponse(status: 200, data: "type2[text/html,somebody09]")
ClientResponse.init(200, "type2[text/html,somebody09]")
),
(
("/test/post", "somebody09", "text/html", "app/type1;q=0.9,app/type2"),
ClientResponse(status: 200, data: "type2[text/html,somebody09]")
ClientResponse.init(200, "type2[text/html,somebody09]")
),
(
("/test/post", "somebody", "text/html", "*/*"),
ClientResponse(status: 200, data: "type1[text/html,somebody]")
ClientResponse.init(200, "type1[text/html,somebody]")
),
(
("/test/post", "somebody", "text/html", ""),
ClientResponse(status: 200, data: "type1[text/html,somebody]")
ClientResponse.init(200, "type1[text/html,somebody]")
),
(
("/test/post", "somebody", "text/html", "app/type2"),
ClientResponse(status: 200, data: "type2[text/html,somebody]")
ClientResponse.init(200, "type2[text/html,somebody]")
),
(
("/test/post", "somebody", "text/html", "app/type3"),
ClientResponse(status: 406, data: "")
ClientResponse.init(406, "")
)
]
var router = RestRouter.init(testValidate)