I am trying to create a web admin dashboard, using Typescript with redux-saga fro frontend and for backend Docker, Golang, Postgres as "main" db and Redis to store access tokens; I followed the idea from this tutorial and its corresponding repo. I am now focusing on the JWT part.
This is the function that handles the signIn request, the one that should give the user a token:
func (h *Handler) signIn(c *gin.Context) {
var req apipayloads.SignInRequestPayload
if ok := bindData(c, &req); !ok {
return
}
u := &models.User{
Email: req.Email,
Password: req.Password,
}
ctx := c.Request.Context()
err := h.UserService.SignIn(ctx, u)
if err != nil {
log.Printf("Failed to sign in user: %v\n", err.Error())
c.JSON(apperrors.Status(err), gin.H{
"error": err,
})
return
}
tokens, err := h.TokenService.NewTokenPair(ctx, u, "")
if err != nil {
log.Printf("Failed to create tokens for user: %v\n", err.Error())
c.JSON(apperrors.Status(err), gin.H{
"error": err,
})
return
}
c.JSON(http.StatusOK, gin.H{
"accessToken": tokens.AccessToken,
"refreshToken": tokens.RefreshToken,
"user": &models.UserPublic{
Name: u.Name,
Surname: u.Surname,
Email: u.Email,
IsAdmin: u.IsAdmin,
},
})
and the NewTokenPair function:
func (s *tokenService) NewTokenPair(ctx context.Context, u *models.User, prevTokenID string) (*models.Token, error) {
// No need to use a repository for idToken as it is unrelated to any data source
idToken, err := utils.GenerateAccessToken(u, s.PrivKey, s.AccessExpiration)
if err != nil {
log.Printf("Error generating accessToken for uid: %v. Error: %v\n", u.UID, err.Error())
return nil, apperrors.NewInternal()
}
refreshToken, err := utils.GenerateRefreshToken(u.UID, s.RefreshSecret, s.RefreshExpiration)
if err != nil {
log.Printf("Error generating refreshToken for uid: %v. Error: %v\n", u.UID, err.Error())
return nil, apperrors.NewInternal()
}
// set freshly minted refresh token to valid list
if err := s.TokenRepository.SetRefreshToken(ctx, u.UID.String(), refreshToken.ID, refreshToken.ExpiresIn); err != nil {
log.Printf("Error storing tokenID for uid: %v. Error: %v\n", u.UID, err.Error())
return nil, apperrors.NewInternal()
}
// delete user's current refresh token (used when refreshing idToken)
if prevTokenID != "" {
if err := s.TokenRepository.DeleteRefreshToken(ctx, u.UID.String(), prevTokenID); err != nil {
log.Printf("Could not delete previous refreshToken for uid: %v, tokenID: %v\n", u.UID.String(), prevTokenID)
}
}
return &models.Token{
AccessToken: idToken,
RefreshToken: refreshToken.SS,
}, nil
}
My problem is that when I call the signin endpoint from Postman, I obtain the correct response. When I call it from the frontend I created with Typescript, I get
Could not SET refresh token to redis for userID/tokenID: 8f1f51fe-87f4-49af-8e1b-d0ec1b8d6504/de20cfa1-18a7-44e5-8abb-ad8009fe39d3: context canceled
I read online that the problem may be due to context.WithTimeout but I tried to replace it with context.Background and nothing changed...and as this is the first time I use both Go and Redis, I don't know where else I can even search for the problem.
Please let me know if you need more details or other code pieces to understand where is the problem.ù
EDIT:
As asked in the comments, this is the part where I use context.WithTimeout():
func Timeout(timeout time.Duration, errTimeout *apperrors.Error) gin.HandlerFunc {
return func(c *gin.Context) {
// set Gin's writer as our custom writer
tw := &timeoutWriter{ResponseWriter: c.Writer, h: make(http.Header)}
c.Writer = tw
// wrap the request context with a timeout
ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
defer cancel()
// update gin request context
c.Request = c.Request.WithContext(ctx)
finished := make(chan struct{}) // to indicate handler finished
panicChan := make(chan interface{}, 1) // used to handle panics if we can't recover
go func() {
defer func() {
if p := recover(); p != nil {
panicChan <- p
}
}()
c.Next() // calls subsequent middleware(s) and handler
finished <- struct{}{}
}()
select {
case <-panicChan:
// if we cannot recover from panic,
// send internal server error
e := apperrors.NewInternal()
tw.ResponseWriter.WriteHeader(e.Status())
eResp, _ := json.Marshal(gin.H{
"error": e,
})
tw.ResponseWriter.Write(eResp)
case <-finished:
// if finished, set headers and write resp
tw.mu.Lock()
defer tw.mu.Unlock()
// map Headers from tw.Header() (written to by gin)
// to tw.ResponseWriter for response
dst := tw.ResponseWriter.Header()
for k, vv := range tw.Header() {
dst[k] = vv
}
tw.ResponseWriter.WriteHeader(tw.code)
// tw.wbuf will have been written to already when gin writes to tw.Write()
tw.ResponseWriter.Write(tw.wbuf.Bytes())
case <-ctx.Done():
// timeout has occurred, send errTimeout and write headers
tw.mu.Lock()
defer tw.mu.Unlock()
// ResponseWriter from gin
tw.ResponseWriter.Header().Set("Content-Type", "application/json")
tw.ResponseWriter.WriteHeader(errTimeout.Status())
eResp, _ := json.Marshal(gin.H{
"error": errTimeout,
})
tw.ResponseWriter.Write(eResp)
c.Abort()
tw.SetTimedOut()
}
}
}
and the, when I create the handler:
type Handler struct {
UserService interfaces.IUserService
TokenService interfaces.ITokenService
}
type Config struct {
R *gin.Engine
UserService interfaces.IUserService
TokenService interfaces.ITokenService
TimeoutDuration time.Duration
}
func NewHandler(c *Config) {
h := &Handler{
UserService: c.UserService,
TokenService: c.TokenService,
}
// Create an account group
auth := c.R.Group(os.Getenv("AUTH_URL"))
if gin.Mode() != gin.TestMode {
auth.Use(middleware.Timeout(c.TimeoutDuration, apperrors.NewServiceUnavailable()))
auth.POST("/signIn", h.signIn)
} else {
auth.POST("/signIn", h.signIn)
}
}
I have also tried to remove this part and just do
auth := c.R.Group(os.Getenv("AUTH_URL"))
auth.POST("/signIn", h.signIn)
But nothing changed.
EDIT 2: Frontend code
I designed a login page where the user needs to insert email and password, and a button to perform the request. This page is on the url http://localhost:3001. After pressing the button, a redirection to http://localhost:3001/email=testdeffo%40test.com&password=test happens. Thisis what the call stack looks like from the developer tools:
The first call uses the method OPTIONS, and the third one is a GET. The middle one looks like this:
This is the saga.ts code:
const login = async (payload: SignInRequestPayload) => {
await axios.post<SignInResponsePayload[]>(
(Config.test ? webapi.testUrl : webapi.prodUrl) + webapi.auth.signIn,
{
...payload,
},
{
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
}
);
};
function* loginSaga(action: any) {
try {
const response: SignInSuccessPayload = yield call(login, {
email: action.payload.email,
password: action.payload.password,
});
console.log("LOG 1")
yield put(signInSuccess(response));
} catch (e: any) {
yield put(
signInFailure({
error: e.message,
})
);
}
}
function* authSaga() {
yield all([takeLatest(SIGNIN_REQUEST, loginSaga)]);
}
export default authSaga;
I did put some console.log around, and what happens is that the code never prints LOG1. I also tried to add a return in the login function like this:
const login = async (payload: SignInRequestPayload) => {
const { data } = await axios.post<SignInResponsePayload>(
(Config.test ? webapi.testUrl : webapi.prodUrl) + webapi.auth.signIn,
{
...payload,
}
);
console.info("LOG2");
return data;
};
But also in this case, LOG2 never gets printed.
EDIT 3
Maybe it can be useful if I also add my backend docker-compose.yaml:
version: "3.8"
services:
reverse-proxy:
# The official v2 Traefik docker image
image: traefik:v2.2
# Enables the web UI and tells Traefik to listen to docker
command:
- "--api.insecure=true"
- "--providers.docker"
- "--providers.docker.exposedByDefault=false"
ports:
# The HTTP port
- "80:80"
# The Web UI (enabled by --api.insecure=true)
- "8080:8080"
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
postgres-auth:
image: "postgres:alpine"
environment:
- POSTGRES_PASSWORD=password
ports:
- "5432:5432"
# Set a volume for data and initial sql script
# May configure initial db for future demo
volumes:
- "pgdata_auth:/var/lib/postgresql/data"
# - ./init:/docker-entrypoint-initdb.d/
command: ["postgres", "-c", "log_statement=all"]
redis-auth:
image: "redis:alpine"
ports:
- "6379:6379"
volumes:
- "redisdata_auth:/data"
apiserver:
build:
context: .
target: builder
image: apiserver
expose:
- "8080"
labels:
- "traefik.enable=true"
- "traefik.http.routers.apiserver.rule=Host(`laboratoriomister.test`) && PathPrefix(`/api/`)"
- "traefik.http.middlewares.cors.headers.accesscontrolalloworigin=*"
- "traefik.http.middlewares.cors.headers.accesscontrolallowmethods=GET,POST,PUT,DELETE,OPTIONS"
- "traefik.http.middlewares.cors.headers.accesscontrolallowheaders=Content-Type"
environment:
- ENV=dev
volumes:
- .:/app
# have to use $$ (double-dollar) so docker doesn't try to substitute a variable
command: reflex -r "\.go$$" -s -- sh -c "go run ./"
env_file: .env
depends_on:
- postgres-auth
- redis-auth
volumes:
pgdata_auth:
redisdata_auth:
Please let me know if you need more information or code pieces. Thanks in advance


A "
context canceled" error should mean the context you are using in the request is canceled before the operation can complete.Your
signInendpoint workflow is:Check if the Redis operation is taking too long and the context is timing out. And make sure the context passed to the Redis operation is not canceled prematurely.
When you call this function, pass in a context that has a reasonable deadline or timeout:
In your TypeScript code, you need to handle responses and potential timeouts:
Finally, make sure your frontend is correctly handling CORS and preflight requests, as this could also lead to canceled contexts if not handled properly. If the preflight request does not succeed due to CORS policy, the actual request might be canceled. See "Issue with CORS that’s been absolutely killing me - I’m sure it’s a basic config issue I’m missing!" by austincollinpena for illustration: that means making sure Traefik is configured to handle CORS requests appropriately:
From the comments, if the browser is canceling the original request due to a navigation event (like a redirect), this would result in the
context cancelederror in Go.So make sure the frontend does not navigate away from the page or redirect before the API call is completed.
In your frontend code, particularly in the submit handler of your form, you should prevent the default form submission behavior that causes the page to reload or navigate away. That is typically done using
event.preventDefault().Removing the middleware does not affect the problem, so that is not an issue. Still, It is a good practice to use the request's context when available, as it is tied to the lifecycle of the request.
context.Background()is a better fit for background tasks and initializations.Attach this
handleSubmitfunction to your form'sonSubmitevent:For your Go server-side code, use the request's context as it allows for better control and cleanup, especially when dealing with user requests that can be terminated by the user at any point.
Only use
context.Background()if there is no request context available or for operations that need to be completely decoupled from the request's lifecycle.