mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
* fix docker build this is trivially incorrect since glide doesn't actually provide reproducible builds. the idea is to build with the deps that we have checked into git, so that we actually know what code is executing so that we might debug it... all for multi stage build instead of what we had, but adding the glide step is wrong. i added a loud warning so as to discourage this behavior in the future. * hang the runner, agent=new sheriff tl;dr agent is now runner, with a hopefully saner api the general idea is get rid of all the various 'task' structs now, change our terminology to only be 'calls' now, push a lot of the http construction of a call into the agent, allow calls to mutate their state around their execution easily and to simplify the number of code paths, channels and context timeouts in something [hopefully] easy to understand. this introduces the idea of 'slots' which are either hot or cold and are separate from reserving memory (memory is denominated in 'tokens' now). a 'slot' is essentially a container that is ready for execution of a call, be it hot or cold (it just means different things based on hotness). taking a look into Submit should make these relatively easy to grok. sorry, things were pretty broken especially wrt timings. I tried to keep good notes (maybe too good), to highlight stuff so that we don't make the same mistakes again (history repeating itself blah blah quote). even now, there is lots of work to do :) I encourage just reading the agent.go code, Submit is really simple and there's a description of how the whole thing works at the head of the file (after TODOs). call.go contains code for constructing calls, as well as Start / End (small atm). I did some amount of code massaging to try to make things simple / straightforward / fit reasonable mental model, but as always am open to critique (the more negative the better) as I'm just one guy and wth do i know... ----------------------------------------------------------------------------- below enumerates a number of changes as briefly as possible (heh..): models.Call all the things removes models.Task as models.Call is now what it previously was. models.FnCall is now rid of in favor of models.Call, despite the datastore only storing a few fields of it [for now]. we should probably store entire calls in the db, since app & route configurations can change at any given moment, it would be nice to see the parameters of each call (costs db space, obviously). this removes the endpoints for getting & deleting messages, we were just looping back to localhost to call the MQ (wtf? this was for iron integration i think) and just calls the MQ. changes the name of the FnLog to LogStore, confusing cause there's also a `FuncLogger` which uses the Logstore (punting). removes other `Fn` prefixed structs (redundant naming convention). removes some unused and/or weird structs (IDStatus, CompleteTime) updates the swagger makes the db methods consistent to use 'Call' nomenclature. remove runner nuisances: * push down registry stuff to docker driver * remove Environment / Stats stuff of yore * remove unused writers (now in FuncLogger) * remove 2 of the task types, old hot stuff, runner, etc fixes ram available calculation on startup to not always be 300GB (helps a lot on a laptop!) format for DOCKER_AUTH env now is not a list but a map (there are no docs, would prefer to get rid of this altogether anyway). the ~/.docker/cfg expected format is unchanged. removes arbitrary task queue, if a machine is out of ram we can probably just time out without queueing... (can open separate discussion) in any case the old one didn't really account well for hot tasks, it just lined everyone up in the task queue if there wasn't a place to run hot and then timed them out [even if a slot became free]. removes HEADER_ prefixing on any headers in the request to a invoke a call. (this was inconsistent with cli for test anyway) removes TASK_ID header sent in to hot only (this is a dupe of FN_CALL_ID, which has not been removed) now user functions can reply directly to the client. this means that for cold containers if they write to stdout it will send a 200 + headers. for hot containers, the user can reply directly to the client from the container, i.e. with its preferred status code / headers (vs. always getting a 200). the dispatch itself is a little http specific atm, i think we can add an interchange format but the current version is easily extended to add json for now, separate discussion. this eliminates a lot of the request/response rewriting and buffering we were doing (yey). now Dispatch ONLY does input and output, vs. managing the call timeout and having access to a call's fields. cache is pushed down into agent now instead of in the front end, would like to push it down to the datastore actually but it's here for now anyway. cache delete functions removed (b/c fn is distributed anyway?). added app caching, should help with latency. in general, a lot of server/runner.go got pushed down into the agent. i think it will be useful in testing to be able to construct calls without having to invoke http handlers + async also needs to construct calls without a handler. safe shutdown actually works now for everything (leaked / didn't wait on certain things before) now we're waiting for hot slots to open up while we're attempting to get ram to launch a container if we didn't find any hot slots to run the call in immediately. we can change this policy really easily now (no more channel jungle; still some channels). also looking for somewhere else to go while the container is launching now. slots now get sent _out_ of a container, vs. a container receiving calls, which makes this kind of policy easier to implement. this fixes a number of bugs around things like trying to execute calls against containers that have not and may never start and trying to launch a bazillion containers when there are no free containers. the driver api underwent some changes to make this possible (relatively minimal, added Wait). the easiest way to think about this is that allocating ram has moved 'up' instead of just wrapping launching containers, so that we can select on a channel trying to find ram. not dispatching hot calls to containers that died anymore either... the timeout is now started at the beginning of Submit, rather than Dispatch or the container itself having to manage the call timeout, which was an inaccurate way of doing things since finding a slot / allocating ram / pulling image can all take a non-trivial (timeout amount, even!) amount of time. this makes for much more reasonable response times from fn under load, there's still a little TODO about handling cold+timeout container removal response times but it's much improved. if call.Start is called with < call.timeout/2 time left, then the call will not be executed and return a timeout. we can discuss. this makes async play _a lot_ nicer, specifically. for large timeouts / 2 makes less sense. env is no longer getting upper cased (admittedly, this can look a little weird now). our whole route.Config/app.Config/env/headers stuff probably deserves a whole discussion... sync output no longer has the call id in json if there's an error / timeout. we could add this back to signify that it's _us_ writing these but this was out of place. FN_CALL_ID is still shipped out to get the id for sync calls, and async [server] output remains unchanged. async logs are now an entire raw http request (so that a user can write a 400 or something from their hot async container) async hot now 'just works' cold sync calls can now reply to the client before container removal, which shaves a lot of latency off of those (still eat start). still need to figure out async removal if timeout or something. ----------------------------------------------------------------------------- i've located a number of bugs that were generally inherited, and also added a number of TODOs in the head of the agent.go file according to robustness we probably need to add. this is at least at parity with the previous implementation, to my knowledge (hopefully/likely a good bit ahead). I can memorialize these to github quickly enough, not that anybody searches before adding bugs anyway (sigh). the big thing to work on next imo is async being a lot more robust, specifically to survive fn server failures / network issues. thanks for review (gulp)
661 lines
16 KiB
Go
661 lines
16 KiB
Go
// Copyright 2013 Julien Schmidt. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be found
|
|
// in the LICENSE file.
|
|
|
|
// TODO what is this, why do we have it, and where can we put it if we need it
|
|
// TODO move to common
|
|
|
|
package agent
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type Handle func(http.ResponseWriter, *http.Request, Params)
|
|
type Param struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
type Params []Param
|
|
|
|
func min(a, b int) int {
|
|
if a <= b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func countParams(path string) uint8 {
|
|
var n uint
|
|
for i := 0; i < len(path); i++ {
|
|
if path[i] != ':' && path[i] != '*' {
|
|
continue
|
|
}
|
|
n++
|
|
}
|
|
if n >= 255 {
|
|
return 255
|
|
}
|
|
return uint8(n)
|
|
}
|
|
|
|
type nodeType uint8
|
|
|
|
const (
|
|
static nodeType = iota // default
|
|
root
|
|
param
|
|
catchAll
|
|
)
|
|
|
|
type node struct {
|
|
path string
|
|
wildChild bool
|
|
nType nodeType
|
|
maxParams uint8
|
|
indices string
|
|
children []*node
|
|
handle Handle
|
|
priority uint32
|
|
}
|
|
|
|
// increments priority of the given child and reorders if necessary
|
|
func (n *node) incrementChildPrio(pos int) int {
|
|
n.children[pos].priority++
|
|
prio := n.children[pos].priority
|
|
|
|
// adjust position (move to front)
|
|
newPos := pos
|
|
for newPos > 0 && n.children[newPos-1].priority < prio {
|
|
// swap node positions
|
|
tmpN := n.children[newPos-1]
|
|
n.children[newPos-1] = n.children[newPos]
|
|
n.children[newPos] = tmpN
|
|
|
|
newPos--
|
|
}
|
|
|
|
// build new index char string
|
|
if newPos != pos {
|
|
n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
|
|
n.indices[pos:pos+1] + // the index char we move
|
|
n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
|
|
}
|
|
|
|
return newPos
|
|
}
|
|
|
|
// addRoute adds a node with the given handle to the path.
|
|
// Not concurrency-safe!
|
|
func (n *node) addRoute(path string, handle Handle) {
|
|
fullPath := path
|
|
n.priority++
|
|
numParams := countParams(path)
|
|
|
|
// non-empty tree
|
|
if len(n.path) > 0 || len(n.children) > 0 {
|
|
walk:
|
|
for {
|
|
// Update maxParams of the current node
|
|
if numParams > n.maxParams {
|
|
n.maxParams = numParams
|
|
}
|
|
|
|
// Find the longest common prefix.
|
|
// This also implies that the common prefix contains no ':' or '*'
|
|
// since the existing key can't contain those chars.
|
|
i := 0
|
|
max := min(len(path), len(n.path))
|
|
for i < max && path[i] == n.path[i] {
|
|
i++
|
|
}
|
|
|
|
// Split edge
|
|
if i < len(n.path) {
|
|
child := node{
|
|
path: n.path[i:],
|
|
wildChild: n.wildChild,
|
|
nType: static,
|
|
indices: n.indices,
|
|
children: n.children,
|
|
handle: n.handle,
|
|
priority: n.priority - 1,
|
|
}
|
|
|
|
// Update maxParams (max of all children)
|
|
for i := range child.children {
|
|
if child.children[i].maxParams > child.maxParams {
|
|
child.maxParams = child.children[i].maxParams
|
|
}
|
|
}
|
|
|
|
n.children = []*node{&child}
|
|
// []byte for proper unicode char conversion, see #65
|
|
n.indices = string([]byte{n.path[i]})
|
|
n.path = path[:i]
|
|
n.handle = nil
|
|
n.wildChild = false
|
|
}
|
|
|
|
// Make new node a child of this node
|
|
if i < len(path) {
|
|
path = path[i:]
|
|
|
|
if n.wildChild {
|
|
n = n.children[0]
|
|
n.priority++
|
|
|
|
// Update maxParams of the child node
|
|
if numParams > n.maxParams {
|
|
n.maxParams = numParams
|
|
}
|
|
numParams--
|
|
|
|
// Check if the wildcard matches
|
|
if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
|
|
// check for longer wildcard, e.g. :name and :names
|
|
if len(n.path) >= len(path) || path[len(n.path)] == '/' {
|
|
continue walk
|
|
}
|
|
}
|
|
|
|
panic("path segment '" + path +
|
|
"' conflicts with existing wildcard '" + n.path +
|
|
"' in path '" + fullPath + "'")
|
|
}
|
|
|
|
c := path[0]
|
|
|
|
// slash after param
|
|
if n.nType == param && c == '/' && len(n.children) == 1 {
|
|
n = n.children[0]
|
|
n.priority++
|
|
continue walk
|
|
}
|
|
|
|
// Check if a child with the next path byte exists
|
|
for i := 0; i < len(n.indices); i++ {
|
|
if c == n.indices[i] {
|
|
i = n.incrementChildPrio(i)
|
|
n = n.children[i]
|
|
continue walk
|
|
}
|
|
}
|
|
|
|
// Otherwise insert it
|
|
if c != ':' && c != '*' {
|
|
// []byte for proper unicode char conversion, see #65
|
|
n.indices += string([]byte{c})
|
|
child := &node{
|
|
maxParams: numParams,
|
|
}
|
|
n.children = append(n.children, child)
|
|
n.incrementChildPrio(len(n.indices) - 1)
|
|
n = child
|
|
}
|
|
n.insertChild(numParams, path, fullPath, handle)
|
|
return
|
|
|
|
} else if i == len(path) { // Make node a (in-path) leaf
|
|
if n.handle != nil {
|
|
panic("a handle is already registered for path '" + fullPath + "'")
|
|
}
|
|
n.handle = handle
|
|
}
|
|
return
|
|
}
|
|
} else { // Empty tree
|
|
n.insertChild(numParams, path, fullPath, handle)
|
|
n.nType = root
|
|
}
|
|
}
|
|
|
|
func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle) {
|
|
var offset int // already handled bytes of the path
|
|
|
|
// find prefix until first wildcard (beginning with ':'' or '*'')
|
|
for i, max := 0, len(path); numParams > 0; i++ {
|
|
c := path[i]
|
|
if c != ':' && c != '*' {
|
|
continue
|
|
}
|
|
|
|
// find wildcard end (either '/' or path end)
|
|
end := i + 1
|
|
for end < max && path[end] != '/' {
|
|
switch path[end] {
|
|
// the wildcard name must not contain ':' and '*'
|
|
case ':', '*':
|
|
panic("only one wildcard per path segment is allowed, has: '" +
|
|
path[i:] + "' in path '" + fullPath + "'")
|
|
default:
|
|
end++
|
|
}
|
|
}
|
|
|
|
// check if this Node existing children which would be
|
|
// unreachable if we insert the wildcard here
|
|
if len(n.children) > 0 {
|
|
panic("wildcard route '" + path[i:end] +
|
|
"' conflicts with existing children in path '" + fullPath + "'")
|
|
}
|
|
|
|
// check if the wildcard has a name
|
|
if end-i < 2 {
|
|
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
|
|
}
|
|
|
|
if c == ':' { // param
|
|
// split path at the beginning of the wildcard
|
|
if i > 0 {
|
|
n.path = path[offset:i]
|
|
offset = i
|
|
}
|
|
|
|
child := &node{
|
|
nType: param,
|
|
maxParams: numParams,
|
|
}
|
|
n.children = []*node{child}
|
|
n.wildChild = true
|
|
n = child
|
|
n.priority++
|
|
numParams--
|
|
|
|
// if the path doesn't end with the wildcard, then there
|
|
// will be another non-wildcard subpath starting with '/'
|
|
if end < max {
|
|
n.path = path[offset:end]
|
|
offset = end
|
|
|
|
child := &node{
|
|
maxParams: numParams,
|
|
priority: 1,
|
|
}
|
|
n.children = []*node{child}
|
|
n = child
|
|
}
|
|
|
|
} else { // catchAll
|
|
if end != max || numParams > 1 {
|
|
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
|
|
}
|
|
|
|
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
|
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
|
|
}
|
|
|
|
// currently fixed width 1 for '/'
|
|
i--
|
|
if path[i] != '/' {
|
|
panic("no / before catch-all in path '" + fullPath + "'")
|
|
}
|
|
|
|
n.path = path[offset:i]
|
|
|
|
// first node: catchAll node with empty path
|
|
child := &node{
|
|
wildChild: true,
|
|
nType: catchAll,
|
|
maxParams: 1,
|
|
}
|
|
n.children = []*node{child}
|
|
n.indices = string(path[i])
|
|
n = child
|
|
n.priority++
|
|
|
|
// second node: node holding the variable
|
|
child = &node{
|
|
path: path[i:],
|
|
nType: catchAll,
|
|
maxParams: 1,
|
|
handle: handle,
|
|
priority: 1,
|
|
}
|
|
n.children = []*node{child}
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
// insert remaining path part and handle to the leaf
|
|
n.path = path[offset:]
|
|
n.handle = handle
|
|
}
|
|
|
|
// Returns the handle registered with the given path (key). The values of
|
|
// wildcards are saved to a map.
|
|
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
|
|
// made if a handle exists with an extra (without the) trailing slash for the
|
|
// given path.
|
|
func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) {
|
|
walk: // outer loop for walking the tree
|
|
for {
|
|
if len(path) > len(n.path) {
|
|
if path[:len(n.path)] == n.path {
|
|
path = path[len(n.path):]
|
|
// If this node does not have a wildcard (param or catchAll)
|
|
// child, we can just look up the next child node and continue
|
|
// to walk down the tree
|
|
if !n.wildChild {
|
|
c := path[0]
|
|
for i := 0; i < len(n.indices); i++ {
|
|
if c == n.indices[i] {
|
|
n = n.children[i]
|
|
continue walk
|
|
}
|
|
}
|
|
|
|
// Nothing found.
|
|
// We can recommend to redirect to the same URL without a
|
|
// trailing slash if a leaf exists for that path.
|
|
tsr = (path == "/" && n.handle != nil)
|
|
return
|
|
|
|
}
|
|
|
|
// handle wildcard child
|
|
n = n.children[0]
|
|
switch n.nType {
|
|
case param:
|
|
// find param end (either '/' or path end)
|
|
end := 0
|
|
for end < len(path) && path[end] != '/' {
|
|
end++
|
|
}
|
|
|
|
// save param value
|
|
if p == nil {
|
|
// lazy allocation
|
|
p = make(Params, 0, n.maxParams)
|
|
}
|
|
i := len(p)
|
|
p = p[:i+1] // expand slice within preallocated capacity
|
|
p[i].Key = n.path[1:]
|
|
p[i].Value = path[:end]
|
|
|
|
// we need to go deeper!
|
|
if end < len(path) {
|
|
if len(n.children) > 0 {
|
|
path = path[end:]
|
|
n = n.children[0]
|
|
continue walk
|
|
}
|
|
|
|
// ... but we can't
|
|
tsr = (len(path) == end+1)
|
|
return
|
|
}
|
|
|
|
if handle = n.handle; handle != nil {
|
|
return
|
|
} else if len(n.children) == 1 {
|
|
// No handle found. Check if a handle for this path + a
|
|
// trailing slash exists for TSR recommendation
|
|
n = n.children[0]
|
|
tsr = (n.path == "/" && n.handle != nil)
|
|
}
|
|
|
|
return
|
|
|
|
case catchAll:
|
|
// save param value
|
|
if p == nil {
|
|
// lazy allocation
|
|
p = make(Params, 0, n.maxParams)
|
|
}
|
|
i := len(p)
|
|
p = p[:i+1] // expand slice within preallocated capacity
|
|
p[i].Key = n.path[2:]
|
|
p[i].Value = path
|
|
|
|
handle = n.handle
|
|
return
|
|
|
|
default:
|
|
panic("invalid node type")
|
|
}
|
|
}
|
|
} else if path == n.path {
|
|
// We should have reached the node containing the handle.
|
|
// Check if this node has a handle registered.
|
|
if handle = n.handle; handle != nil {
|
|
return
|
|
}
|
|
|
|
if path == "/" && n.wildChild && n.nType != root {
|
|
tsr = true
|
|
return
|
|
}
|
|
|
|
// No handle found. Check if a handle for this path + a
|
|
// trailing slash exists for trailing slash recommendation
|
|
for i := 0; i < len(n.indices); i++ {
|
|
if n.indices[i] == '/' {
|
|
n = n.children[i]
|
|
tsr = (len(n.path) == 1 && n.handle != nil) ||
|
|
(n.nType == catchAll && n.children[0].handle != nil)
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Nothing found. We can recommend to redirect to the same URL with an
|
|
// extra trailing slash if a leaf exists for that path
|
|
tsr = (path == "/") ||
|
|
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
|
|
path == n.path[:len(n.path)-1] && n.handle != nil)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Makes a case-insensitive lookup of the given path and tries to find a handler.
|
|
// It can optionally also fix trailing slashes.
|
|
// It returns the case-corrected path and a bool indicating whether the lookup
|
|
// was successful.
|
|
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) {
|
|
return n.findCaseInsensitivePathRec(
|
|
path,
|
|
strings.ToLower(path),
|
|
make([]byte, 0, len(path)+1), // preallocate enough memory for new path
|
|
[4]byte{}, // empty rune buffer
|
|
fixTrailingSlash,
|
|
)
|
|
}
|
|
|
|
// shift bytes in array by n bytes left
|
|
func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
|
|
switch n {
|
|
case 0:
|
|
return rb
|
|
case 1:
|
|
return [4]byte{rb[1], rb[2], rb[3], 0}
|
|
case 2:
|
|
return [4]byte{rb[2], rb[3]}
|
|
case 3:
|
|
return [4]byte{rb[3]}
|
|
default:
|
|
return [4]byte{}
|
|
}
|
|
}
|
|
|
|
// recursive case-insensitive lookup function used by n.findCaseInsensitivePath
|
|
func (n *node) findCaseInsensitivePathRec(path, loPath string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) ([]byte, bool) {
|
|
loNPath := strings.ToLower(n.path)
|
|
|
|
walk: // outer loop for walking the tree
|
|
for len(loPath) >= len(loNPath) && (len(loNPath) == 0 || loPath[1:len(loNPath)] == loNPath[1:]) {
|
|
// add common path to result
|
|
ciPath = append(ciPath, n.path...)
|
|
|
|
if path = path[len(n.path):]; len(path) > 0 {
|
|
loOld := loPath
|
|
loPath = loPath[len(loNPath):]
|
|
|
|
// If this node does not have a wildcard (param or catchAll) child,
|
|
// we can just look up the next child node and continue to walk down
|
|
// the tree
|
|
if !n.wildChild {
|
|
// skip rune bytes already processed
|
|
rb = shiftNRuneBytes(rb, len(loNPath))
|
|
|
|
if rb[0] != 0 {
|
|
// old rune not finished
|
|
for i := 0; i < len(n.indices); i++ {
|
|
if n.indices[i] == rb[0] {
|
|
// continue with child node
|
|
n = n.children[i]
|
|
loNPath = strings.ToLower(n.path)
|
|
continue walk
|
|
}
|
|
}
|
|
} else {
|
|
// process a new rune
|
|
var rv rune
|
|
|
|
// find rune start
|
|
// runes are up to 4 byte long,
|
|
// -4 would definitely be another rune
|
|
var off int
|
|
for max := min(len(loNPath), 3); off < max; off++ {
|
|
if i := len(loNPath) - off; utf8.RuneStart(loOld[i]) {
|
|
// read rune from cached lowercase path
|
|
rv, _ = utf8.DecodeRuneInString(loOld[i:])
|
|
break
|
|
}
|
|
}
|
|
|
|
// calculate lowercase bytes of current rune
|
|
utf8.EncodeRune(rb[:], rv)
|
|
// skipp already processed bytes
|
|
rb = shiftNRuneBytes(rb, off)
|
|
|
|
for i := 0; i < len(n.indices); i++ {
|
|
// lowercase matches
|
|
if n.indices[i] == rb[0] {
|
|
// must use a recursive approach since both the
|
|
// uppercase byte and the lowercase byte might exist
|
|
// as an index
|
|
if out, found := n.children[i].findCaseInsensitivePathRec(
|
|
path, loPath, ciPath, rb, fixTrailingSlash,
|
|
); found {
|
|
return out, true
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// same for uppercase rune, if it differs
|
|
if up := unicode.ToUpper(rv); up != rv {
|
|
utf8.EncodeRune(rb[:], up)
|
|
rb = shiftNRuneBytes(rb, off)
|
|
|
|
for i := 0; i < len(n.indices); i++ {
|
|
// uppercase matches
|
|
if n.indices[i] == rb[0] {
|
|
// continue with child node
|
|
n = n.children[i]
|
|
loNPath = strings.ToLower(n.path)
|
|
continue walk
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Nothing found. We can recommend to redirect to the same URL
|
|
// without a trailing slash if a leaf exists for that path
|
|
return ciPath, (fixTrailingSlash && path == "/" && n.handle != nil)
|
|
}
|
|
|
|
n = n.children[0]
|
|
switch n.nType {
|
|
case param:
|
|
// find param end (either '/' or path end)
|
|
k := 0
|
|
for k < len(path) && path[k] != '/' {
|
|
k++
|
|
}
|
|
|
|
// add param value to case insensitive path
|
|
ciPath = append(ciPath, path[:k]...)
|
|
|
|
// we need to go deeper!
|
|
if k < len(path) {
|
|
if len(n.children) > 0 {
|
|
// continue with child node
|
|
n = n.children[0]
|
|
loNPath = strings.ToLower(n.path)
|
|
loPath = loPath[k:]
|
|
path = path[k:]
|
|
continue
|
|
}
|
|
|
|
// ... but we can't
|
|
if fixTrailingSlash && len(path) == k+1 {
|
|
return ciPath, true
|
|
}
|
|
return ciPath, false
|
|
}
|
|
|
|
if n.handle != nil {
|
|
return ciPath, true
|
|
} else if fixTrailingSlash && len(n.children) == 1 {
|
|
// No handle found. Check if a handle for this path + a
|
|
// trailing slash exists
|
|
n = n.children[0]
|
|
if n.path == "/" && n.handle != nil {
|
|
return append(ciPath, '/'), true
|
|
}
|
|
}
|
|
return ciPath, false
|
|
|
|
case catchAll:
|
|
return append(ciPath, path...), true
|
|
|
|
default:
|
|
panic("invalid node type")
|
|
}
|
|
} else {
|
|
// We should have reached the node containing the handle.
|
|
// Check if this node has a handle registered.
|
|
if n.handle != nil {
|
|
return ciPath, true
|
|
}
|
|
|
|
// No handle found.
|
|
// Try to fix the path by adding a trailing slash
|
|
if fixTrailingSlash {
|
|
for i := 0; i < len(n.indices); i++ {
|
|
if n.indices[i] == '/' {
|
|
n = n.children[i]
|
|
if (len(n.path) == 1 && n.handle != nil) ||
|
|
(n.nType == catchAll && n.children[0].handle != nil) {
|
|
return append(ciPath, '/'), true
|
|
}
|
|
return ciPath, false
|
|
}
|
|
}
|
|
}
|
|
return ciPath, false
|
|
}
|
|
}
|
|
|
|
// Nothing found.
|
|
// Try to fix the path by adding / removing a trailing slash
|
|
if fixTrailingSlash {
|
|
if path == "/" {
|
|
return ciPath, true
|
|
}
|
|
if len(loPath)+1 == len(loNPath) && loNPath[len(loPath)] == '/' &&
|
|
loPath[1:] == loNPath[1:len(loPath)] && n.handle != nil {
|
|
return append(ciPath, n.path...), true
|
|
}
|
|
}
|
|
return ciPath, false
|
|
}
|