从 Typescript 调用时取消了 Go-redis 上下文

问题描述 投票:0回答:1

我正在尝试创建一个 Web 管理仪表板,使用带有 redux-saga 的 Typescript 前端和后端 Docker、Golang、Postgres 作为“主”数据库和 Redis 来存储访问令牌;我遵循了这个tutorial及其相应的repo的想法。我现在专注于 JWT 部分。

这是处理登录请求的函数,应该为用户提供令牌:

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,
        },
    })

和 NewTokenPair 函数:

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
}

我的问题是,当我从 Postman 调用登录端点时,我获得了正确的响应。当我从用 Typescript 创建的前端调用它时,我得到

Could not SET refresh token to redis for userID/tokenID: 8f1f51fe-87f4-49af-8e1b-d0ec1b8d6504/de20cfa1-18a7-44e5-8abb-ad8009fe39d3: context canceled

我在网上看到问题可能是由于

context.WithTimeout
造成的,但我尝试用
context.Background
替换它,但没有任何改变......而且由于这是我第一次同时使用Go和Redis,我不知道在哪里否则我什至可以搜索问题。

如果您需要更多详细信息或其他代码片段来了解问题出在哪里,请告诉我。ù

编辑

正如评论中所问,这是我使用 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()
        }
    }
}

以及,当我创建处理程序时:

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)
    }
}

我也尝试过删除这部分,然后就这样做了

auth := c.R.Group(os.Getenv("AUTH_URL"))
auth.POST("/signIn", h.signIn)

但什么都没有改变。

编辑2:前端代码

我设计了一个登录页面,用户需要在其中插入电子邮件和密码,以及一个执行请求的按钮。此页面位于 url http://localhost:3001 上。按下按钮后,会重定向到 http://localhost:3001/email=testdeffo%40test.com&password=test。这是开发人员工具中的调用堆栈的样子:

第一个调用使用 OPTIONS 方法,第三个调用是 GET。中间的那个是这样的:

这是 saga.ts 代码:

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;

我确实放了一些console.log,结果代码永远不会打印LOG1。我还尝试在登录功能中添加返回,如下所示:

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;
};

但同样在这种情况下,LOG2 永远不会被打印。

编辑3

如果我还添加我的后端,也许它会很有用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:

如果您需要更多信息或代码片段,请告诉我。 预先感谢

typescript docker go redux-saga go-gin
1个回答
0
投票

context canceled
”错误应该意味着您在请求中使用的上下文在操作完成之前被取消
您的
signIn
端点工作流程是:

[Client (Typescript)] --HTTP Request--> [Go Server] --Set Token--> [Redis]

检查Redis操作是否耗时过长,上下文超时。并确保传递给 Redis 操作的上下文不会提前取消。

func (s *tokenService) setRefreshTokenWithCtx(ctx context.Context, userID, tokenID string, expiresIn time.Duration) error {
    // That will block until the token is set or the context is canceled
    err := s.TokenRepository.SetRefreshToken(ctx, userID, tokenID, expiresIn)
    if err != nil {
        return fmt.Errorf("could not SET refresh token to redis for userID/tokenID: %s/%s: %v", userID, tokenID, err)
    }
    return nil
}

当您调用此函数时,传入具有合理截止日期或超时的上下文:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

err := setRefreshTokenWithCtx(ctx, u.UID.String(), refreshToken.ID, refreshToken.ExpiresIn)
if err != nil {
    log.Printf("Error: %v\n", err)
    // Handle the error, possibly by returning a specific error to the client
}

在 TypeScript 代码中,您需要处理响应和潜在的超时:

const login = async (payload: SignInRequestPayload): Promise<SignInResponsePayload> => {
  try {
    const response = await axios.post<SignInResponsePayload>(
      `${Config.test ? webapi.testUrl : webapi.prodUrl}${webapi.auth.signIn}`,
      payload,
      {
        headers: {
          "Content-Type": "application/json",
          Accept: "application/json",
        },
        timeout: 5000, // Set a timeout for the request
      }
    );
    return response.data;
  } catch (error) {
    console.error("An error occurred during login:", error);
    throw error;
  }
};

最后,确保您的前端正确处理 CORS 和预检请求,因为如果处理不当,这也可能导致上下文被取消。如果预检请求由于 CORS 策略而未成功,则实际请求可能会被取消。请参阅 austincollinpena 的“ CORS 问题,这绝对要了我的命 - 我确定这是我缺少的基本配置问题!”作为说明:这意味着确保 Traefik 配置为正确处理 CORS 请求:

labels:
  - "traefik.http.middlewares.myapp-cors.headers.accesscontrolallowmethods=GET,POST,PUT,DELETE,OPTIONS"
  - "traefik.http.middlewares.myapp-cors.headers.accesscontrolallowheaders=*"
  - "traefik.http.middlewares.myapp-cors.headers.accesscontrolalloworigin=*"
  - "traefik.http.routers.myapp.middlewares=myapp-cors"
© www.soinside.com 2019 - 2024. All rights reserved.