Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow duplicate query keys #2

Merged
merged 21 commits into from
Jan 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a4111d9
Add common intellij paths to .gitignore
torbencarstens Sep 18, 2023
ad46aa4
Use 1.6.14 in CI instead of pre-1.6.0 git sha
torbencarstens Sep 18, 2023
08bec4a
Add 2.0.0 to test matrix
torbencarstens Sep 18, 2023
2fe8aa7
Only test nim-in-action-code/Chapter7 for nim versions < 2.0
torbencarstens Sep 18, 2023
c631652
Bump actions/checkout and iffy/install-nim
torbencarstens Sep 18, 2023
f05bd2b
Run 'git submodule update --init' for 'nimble test' to ensure that al…
torbencarstens Sep 18, 2023
88b2ace
Add 'nimble refresh' and 'nimble install' for 'nimble test' task befo…
torbencarstens Sep 18, 2023
5c9e5d3
Remove trailing whitespace
torbencarstens Sep 18, 2023
fef495a
Add support for duplicate keys in parameters by adding `paramValuesAs…
torbencarstens Sep 18, 2023
b8374b4
Adapt `params` to use `cgi.decodeData` instead of `parseUrlQuery`
torbencarstens Sep 18, 2023
f916b25
Add tests for #247
torbencarstens Sep 18, 2023
d330fad
Fix argument name to iffy/install-nim
torbencarstens Sep 18, 2023
1cce8cc
Auto accept nimble for refresh/install
torbencarstens Sep 18, 2023
944dc39
Run debug actions in CI
torbencarstens Sep 18, 2023
149d5bf
Only debug 2.0.0
torbencarstens Sep 18, 2023
88e4a29
Remove debug actions
torbencarstens Sep 18, 2023
cda4d39
Run nim2 with '--mm:refc'
torbencarstens Sep 18, 2023
85b2692
Remove nim2 from CI
torbencarstens Sep 18, 2023
84035bf
Remove debug when
torbencarstens Sep 18, 2023
44d5868
Revert "Add common intellij paths to .gitignore"
torbencarstens Sep 19, 2023
c824fef
Merge branch 'master' into allowDuplicateQueryKeys
ThomasTJdev Jan 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 46 additions & 11 deletions jester/request.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import uri, cgi, tables, logging, strutils, re, options
from sequtils import map

import jester/private/utils

Expand Down Expand Up @@ -93,28 +94,62 @@ proc ip*(req: Request): string =

proc params*(req: Request): Table[string, string] =
## Parameters from the pattern and the query string.
##
## Note that this doesn't allow for duplicated keys (it simply returns the last occuring value)
## Use `paramValuesAsSeq` if you need multiple values for a key
if req.patternParams.isSome():
result = req.patternParams.get()
else:
result = initTable[string, string]()

when useHttpBeast:
let query = req.req.path.get("").parseUri().query
var queriesToDecode: seq[string] = @[]
queriesToDecode.add query(req)

let contentType = req.headers.getOrDefault("Content-Type")
if contentType.startswith("application/x-www-form-urlencoded"):
queriesToDecode.add req.body

for query in queriesToDecode:
try:
for key, val in cgi.decodeData(query):
result[key] = decodeUrl(val)
except CgiError:
logging.warn("Incorrect query. Got: $1" % [query])

proc paramValuesAsSeq*(req: Request): Table[string, seq[string]] =
## Parameters from the pattern and the query string.
##
## This allows for duplicated keys in the query (in contrast to `params`)
if req.patternParams.isSome():
let patternParams: Table[string, string] = req.patternParams.get()
var patternParamsSeq: seq[(string, string)] = @[]
for key, val in pairs(patternParams):
patternParamsSeq.add (key, val)

# We are not url-decoding the key/value for the patternParams (matches implementation in `params`
result = sequtils.map(patternParamsSeq,
proc(entry: (string, string)): (string, seq[string]) =
(entry[0], @[entry[1]])
).toTable()
else:
let query = req.req.url.query
result = initTable[string, seq[string]]()

try:
for key, val in cgi.decodeData(query):
result[key] = decodeUrl(val)
except CgiError:
logging.warn("Incorrect query. Got: $1" % [query])
var queriesToDecode: seq[string] = @[]
queriesToDecode.add query(req)

let contentType = req.headers.getOrDefault("Content-Type")
if contentType.startswith("application/x-www-form-urlencoded"):
queriesToDecode.add req.body

for query in queriesToDecode:
try:
parseUrlQuery(req.body, result)
except:
logging.warn("Could not parse URL query.")
for key, value in cgi.decodeData(query):
if result.hasKey(key):
result[key].add value
else:
result[key] = @[value]
except CgiError:
logging.warn("Incorrect query. Got: $1" % [query])

proc formData*(req: Request): MultiData =
let contentType = req.headers.getOrDefault("Content-Type")
Expand Down
41 changes: 41 additions & 0 deletions tests/issue247.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from std/cgi import decodeUrl
from std/strformat import fmt
from std/strutils import join
import jester

settings:
port = Port(5454)
bindAddr = "127.0.0.1"

proc formatParams(params: Table[string, string]): string =
result = ""
for key, value in params.pairs:
result.add fmt"{key}: {value}"

proc formatSeqParams(params: Table[string, seq[string]]): string =
result = ""
for key, values in params.pairs:
let value = values.join ","
result.add fmt"{key}: {value}"

routes:
get "/":
resp Http200
get "/params":
let params = params request
resp formatParams params
get "/params/@val%23ue":
let params = params request
resp formatParams params
post "/params/@val%23ue":
let params = params request
resp formatParams params
get "/multi":
let params = paramValuesAsSeq request
resp formatSeqParams(params)
get "/@val%23ue":
let params = paramValuesAsSeq request
resp formatSeqParams(params)
post "/@val%23ue":
let params = paramValuesAsSeq request
resp formatSeqParams(params)
97 changes: 97 additions & 0 deletions tests/tester.nim
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,101 @@ proc customRouterTest(useStdLib: bool) =
check resp.headers["location"] == address & "/404"
check (waitFor resp.body) == ""

proc issue247(useStdLib: bool) =
waitFor startServer("issue247.nim", useStdLib)
var client = newAsyncHttpClient(maxRedirects = 0)

suite "issue247 useStdLib=" & $useStdLib:
test "duplicate keys in query":
let resp = waitFor client.get(address & "/multi?a=1&a=2")
check (waitFor resp.body) == "a: 1,2"

test "no duplicate keys in query":
let resp = waitFor client.get(address & "/multi?a=1")
check (waitFor resp.body) == "a: 1"

test "assure that empty values are handled":
let resp = waitFor client.get(address & "/multi?a=1&a=")
check (waitFor resp.body) == "a: 1,"

test "assure that fragment is not parsed":
let resp = waitFor client.get(address & "/multi?a=1&#a=2")
check (waitFor resp.body) == "a: 1"

test "ensure that values are url decoded per default":
let resp = waitFor client.get(address & "/multi?a=1&a=1%232")
check (waitFor resp.body) == "a: 1,1#2"

test "ensure that keys are url decoded per default":
let resp = waitFor client.get(address & "/multi?a%23b=1&a%23b=1%232")
check (waitFor resp.body) == "a#b: 1,1#2"

test "test different keys":
let resp = waitFor client.get(address & "/multi?a=1&b=2")
check (waitFor resp.body) == "b: 2a: 1"

test "ensure that path params aren't escaped":
let resp = waitFor client.get(address & "/hello%23world")
check (waitFor resp.body) == "val%23ue: hello%23world"

test "test path params and query":
let resp = waitFor client.get(address & "/hello%23world?a%23+b=1%23+b")
check (waitFor resp.body) == "a# b: 1# bval%23ue: hello%23world"

test "test percent encoded path param and query param (same key)":
let resp = waitFor client.get(address & "/hello%23world?val%23ue=1%23+b")
check (waitFor resp.body) == "val%23ue: hello%23worldval#ue: 1# b"

test "test path param, query param and x-www-form-urlencoded":
client.headers = newHttpHeaders({"Content-Type": "application/x-www-form-urlencoded"})
let resp = waitFor client.post(address & "/hello%23world?val%23ue=1%23+b", "val%23ue=1%23+b&b=2")
check (waitFor resp.body) == "val%23ue: hello%23worldb: 2val#ue: 1# b,1# b"

test "params duplicate keys in query":
let resp = waitFor client.get(address & "/params?a=1&a=2")
check (waitFor resp.body) == "a: 2"

test "params no duplicate keys in query":
let resp = waitFor client.get(address & "/params?a=1")
check (waitFor resp.body) == "a: 1"

test "params assure that empty values are handled":
let resp = waitFor client.get(address & "/params?a=1&a=")
check (waitFor resp.body) == "a: "

test "params assure that fragment is not parsed":
let resp = waitFor client.get(address & "/params?a=1&#a=2")
check (waitFor resp.body) == "a: 1"

test "params ensure that values are url decoded per default":
let resp = waitFor client.get(address & "/params?a=1&a=1%232")
check (waitFor resp.body) == "a: 1#2"

test "params ensure that keys are url decoded per default":
let resp = waitFor client.get(address & "/params?a%23b=1&a%23b=1%232")
check (waitFor resp.body) == "a#b: 1#2"

test "params test different keys":
let resp = waitFor client.get(address & "/params?a=1&b=2")
check (waitFor resp.body) == "b: 2a: 1"

test "params ensure that path params aren't escaped":
let resp = waitFor client.get(address & "/params/hello%23world")
check (waitFor resp.body) == "val%23ue: hello%23world"

test "params test path params and query":
let resp = waitFor client.get(address & "/params/hello%23world?a%23+b=1%23+b")
check (waitFor resp.body) == "a# b: 1# bval%23ue: hello%23world"

test "params test percent encoded path param and query param (same key)":
let resp = waitFor client.get(address & "/params/hello%23world?val%23ue=1%23+b")
check (waitFor resp.body) == "val#ue: 1# bval%23ue: hello%23world"

test "params test path param, query param and x-www-form-urlencoded":
client.headers = newHttpHeaders({"Content-Type": "application/x-www-form-urlencoded"})
let resp = waitFor client.post(address & "/params/hello%23world?val%23ue=1%23+b", "val%23ue=1%23+b&b=2")
check (waitFor resp.body) == "b: 2val#ue: 1# bval%23ue: hello%23world"

when isMainModule:
try:
allTest(useStdLib=false) # Test HttpBeast.
Expand All @@ -286,6 +381,8 @@ when isMainModule:
issue150(useStdLib=true)
customRouterTest(useStdLib=false)
customRouterTest(useStdLib=true)
issue247(useStdLib=false)
issue247(useStdLib=true)

# Verify that Nim in Action Tweeter still compiles.
test "Nim in Action - Tweeter":
Expand Down