[{"data":1,"prerenderedAt":3105},["ShallowReactive",2],{"content:\u002F07-rpc-grpc\u002F04-grpc-context-errors":3},{"title":4,"description":5,"path":6,"body":7},"gRPC: context, deadlines и ошибки","В Go вы уже видели context.Context и errors. В gRPC эти темы становятся частью транспортного контракта: клиент задаёт deadline, сервер видит отмену через context, а ошибка превращается в gRPC status code.","\u002F07-rpc-grpc\u002F04-grpc-context-errors",{"type":8,"value":9,"toc":3090},"minimark",[10,14,23,34,37,42,48,243,250,266,269,271,275,470,481,486,500,503,633,636,639,693,696,698,702,705,816,819,822,979,989,991,995,1001,1004,1124,1127,1165,1171,1174,1199,1202,1401,1403,1407,1419,1607,1610,1645,1648,1703,1710,1713,1730,1733,1747,1758,1760,1764,1771,1941,1944,1995,1998,2253,2256,2258,2262,2265,2268,2341,2344,2478,2481,2483,2487,2493,2540,2543,2565,2567,2571,2614,2616,2620,2679,2681,2685,2723,2921,3086],[11,12,4],"h1",{"id":13},"grpc-context-deadlines-и-ошибки",[15,16,17,18,22],"p",{},"В Go вы уже видели ",[19,20,21],"code",{},"context.Context"," и errors. В gRPC эти темы становятся частью транспортного контракта: клиент задаёт deadline, сервер видит отмену через context, а ошибка превращается в gRPC status code.",[15,24,25,26,29,30,33],{},"Этот урок не повторяет базовый ",[19,27,28],{},"context.WithTimeout"," и ",[19,31,32],{},"%w",". Здесь фокус на том, как они проходят через gRPC.",[35,36],"hr",{},[38,39,41],"h2",{"id":40},"context-в-unary-rpc","Context в unary RPC",[15,43,44,45,47],{},"Сигнатура unary handler всегда получает ",[19,46,21],{},":",[49,50,55],"pre",{"className":51,"code":52,"language":53,"meta":54,"style":54},"language-go shiki shiki-themes github-dark","func (s *LessonServer) GetLesson(\n    ctx context.Context,\n    req *coursev1.GetLessonRequest,\n) (*coursev1.GetLessonResponse, error) {\n    lesson, err := s.usecase.GetLesson(ctx, req.GetSlug())\n    if err != nil {\n        return nil, toGRPCError(err)\n    }\n\n    return toProtoLesson(lesson), nil\n}\n","go","",[19,56,57,90,108,127,151,174,193,209,215,222,237],{"__ignoreMap":54},[58,59,62,66,70,74,77,81,84,87],"span",{"class":60,"line":61},"line",1,[58,63,65],{"class":64},"snl16","func",[58,67,69],{"class":68},"s95oV"," (",[58,71,73],{"class":72},"s9osk","s ",[58,75,76],{"class":64},"*",[58,78,80],{"class":79},"svObZ","LessonServer",[58,82,83],{"class":68},") ",[58,85,86],{"class":79},"GetLesson",[58,88,89],{"class":68},"(\n",[58,91,93,96,99,102,105],{"class":60,"line":92},2,[58,94,95],{"class":72},"    ctx",[58,97,98],{"class":79}," context",[58,100,101],{"class":68},".",[58,103,104],{"class":79},"Context",[58,106,107],{"class":68},",\n",[58,109,111,114,117,120,122,125],{"class":60,"line":110},3,[58,112,113],{"class":72},"    req",[58,115,116],{"class":64}," *",[58,118,119],{"class":79},"coursev1",[58,121,101],{"class":68},[58,123,124],{"class":79},"GetLessonRequest",[58,126,107],{"class":68},[58,128,130,133,135,137,139,142,145,148],{"class":60,"line":129},4,[58,131,132],{"class":68},") (",[58,134,76],{"class":64},[58,136,119],{"class":79},[58,138,101],{"class":68},[58,140,141],{"class":79},"GetLessonResponse",[58,143,144],{"class":68},", ",[58,146,147],{"class":64},"error",[58,149,150],{"class":68},") {\n",[58,152,154,157,160,163,165,168,171],{"class":60,"line":153},5,[58,155,156],{"class":68},"    lesson, err ",[58,158,159],{"class":64},":=",[58,161,162],{"class":68}," s.usecase.",[58,164,86],{"class":79},[58,166,167],{"class":68},"(ctx, req.",[58,169,170],{"class":79},"GetSlug",[58,172,173],{"class":68},"())\n",[58,175,177,180,183,186,190],{"class":60,"line":176},6,[58,178,179],{"class":64},"    if",[58,181,182],{"class":68}," err ",[58,184,185],{"class":64},"!=",[58,187,189],{"class":188},"sDLfK"," nil",[58,191,192],{"class":68}," {\n",[58,194,196,199,201,203,206],{"class":60,"line":195},7,[58,197,198],{"class":64},"        return",[58,200,189],{"class":188},[58,202,144],{"class":68},[58,204,205],{"class":79},"toGRPCError",[58,207,208],{"class":68},"(err)\n",[58,210,212],{"class":60,"line":211},8,[58,213,214],{"class":68},"    }\n",[58,216,218],{"class":60,"line":217},9,[58,219,221],{"emptyLinePlaceholder":220},true,"\n",[58,223,225,228,231,234],{"class":60,"line":224},10,[58,226,227],{"class":64},"    return",[58,229,230],{"class":79}," toProtoLesson",[58,232,233],{"class":68},"(lesson), ",[58,235,236],{"class":188},"nil\n",[58,238,240],{"class":60,"line":239},11,[58,241,242],{"class":68},"}\n",[15,244,245,246,249],{},"Этот ",[19,247,248],{},"ctx"," несёт:",[251,252,253,257,260,263],"ul",{},[254,255,256],"li",{},"deadline, если клиент его поставил;",[254,258,259],{},"cancellation, если клиент отменил запрос или соединение закрылось;",[254,261,262],{},"metadata, например auth token или request id;",[254,264,265],{},"значения, которые добавили interceptors.",[15,267,268],{},"Правило: context приходит сверху и передаётся вниз. Его не хранят в struct и не создают заново без причины.",[35,270],{},[38,272,274],{"id":273},"deadline-на-клиенте","Deadline на клиенте",[49,276,278],{"className":51,"code":277,"language":53,"meta":54,"style":54},"ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)\ndefer cancel()\n\nresp, err := client.GetLesson(ctx, &coursev1.GetLessonRequest{\n    Slug: \"grpc-context-errors\",\n})\nif err != nil {\n    st, ok := status.FromError(err)\n    if ok && st.Code() == codes.DeadlineExceeded {\n        log.Println(\"server did not answer before deadline\")\n    }\n    return\n}\n\n_ = resp\n",[19,279,280,310,321,325,352,363,368,381,396,421,438,442,448,453,458],{"__ignoreMap":54},[58,281,282,285,287,290,293,296,299,302,305,307],{"class":60,"line":61},[58,283,284],{"class":68},"ctx, cancel ",[58,286,159],{"class":64},[58,288,289],{"class":68}," context.",[58,291,292],{"class":79},"WithTimeout",[58,294,295],{"class":68},"(context.",[58,297,298],{"class":79},"Background",[58,300,301],{"class":68},"(), ",[58,303,304],{"class":188},"2",[58,306,76],{"class":64},[58,308,309],{"class":68},"time.Second)\n",[58,311,312,315,318],{"class":60,"line":92},[58,313,314],{"class":64},"defer",[58,316,317],{"class":79}," cancel",[58,319,320],{"class":68},"()\n",[58,322,323],{"class":60,"line":110},[58,324,221],{"emptyLinePlaceholder":220},[58,326,327,330,332,335,337,340,343,345,347,349],{"class":60,"line":129},[58,328,329],{"class":68},"resp, err ",[58,331,159],{"class":64},[58,333,334],{"class":68}," client.",[58,336,86],{"class":79},[58,338,339],{"class":68},"(ctx, ",[58,341,342],{"class":64},"&",[58,344,119],{"class":79},[58,346,101],{"class":68},[58,348,124],{"class":79},[58,350,351],{"class":68},"{\n",[58,353,354,357,361],{"class":60,"line":153},[58,355,356],{"class":68},"    Slug: ",[58,358,360],{"class":359},"sU2Wk","\"grpc-context-errors\"",[58,362,107],{"class":68},[58,364,365],{"class":60,"line":176},[58,366,367],{"class":68},"})\n",[58,369,370,373,375,377,379],{"class":60,"line":195},[58,371,372],{"class":64},"if",[58,374,182],{"class":68},[58,376,185],{"class":64},[58,378,189],{"class":188},[58,380,192],{"class":68},[58,382,383,386,388,391,394],{"class":60,"line":211},[58,384,385],{"class":68},"    st, ok ",[58,387,159],{"class":64},[58,389,390],{"class":68}," status.",[58,392,393],{"class":79},"FromError",[58,395,208],{"class":68},[58,397,398,400,403,406,409,412,415,418],{"class":60,"line":217},[58,399,179],{"class":64},[58,401,402],{"class":68}," ok ",[58,404,405],{"class":64},"&&",[58,407,408],{"class":68}," st.",[58,410,411],{"class":79},"Code",[58,413,414],{"class":68},"() ",[58,416,417],{"class":64},"==",[58,419,420],{"class":68}," codes.DeadlineExceeded {\n",[58,422,423,426,429,432,435],{"class":60,"line":224},[58,424,425],{"class":68},"        log.",[58,427,428],{"class":79},"Println",[58,430,431],{"class":68},"(",[58,433,434],{"class":359},"\"server did not answer before deadline\"",[58,436,437],{"class":68},")\n",[58,439,440],{"class":60,"line":239},[58,441,214],{"class":68},[58,443,445],{"class":60,"line":444},12,[58,446,447],{"class":64},"    return\n",[58,449,451],{"class":60,"line":450},13,[58,452,242],{"class":68},[58,454,456],{"class":60,"line":455},14,[58,457,221],{"emptyLinePlaceholder":220},[58,459,461,464,467],{"class":60,"line":460},15,[58,462,463],{"class":68},"_ ",[58,465,466],{"class":64},"=",[58,468,469],{"class":68}," resp\n",[15,471,472,473,476,477,480],{},"Deadline - это не \"совет\". Когда время истекает, gRPC завершает RPC. Клиент получает ",[19,474,475],{},"codes.DeadlineExceeded",". Сервер может тоже увидеть отмену через ",[19,478,479],{},"ctx.Done()",", но уже сделанные побочные эффекты автоматически не откатываются.",[482,483,485],"h3",{"id":484},"deadline-как-часть-контракта-между-сервисами","Deadline как часть контракта между сервисами",[15,487,488,489,492,493,496,497,499],{},"Внутренний gRPC-вызов почти никогда не должен идти с бесконечным временем ожидания. Если сервис ",[19,490,491],{},"A"," обрабатывает внешний запрос за 3 секунды и внутри зовёт сервис ",[19,494,495],{},"B",", у ",[19,498,495],{}," не может быть своего \"сколько получится\". Он получает остаток времени через context.",[15,501,502],{},"Иногда конкретному downstream call ставят ещё более короткий timeout:",[49,504,506],{"className":51,"code":505,"language":53,"meta":54,"style":54},"func (uc *UseCase) CompleteLesson(ctx context.Context, slug string) error {\n    ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)\n    defer cancel()\n\n    _, err := uc.progressClient.MarkCompleted(ctx, &progressv1.MarkCompletedRequest{\n        Slug: slug,\n    })\n    return err\n}\n",[19,507,508,551,572,581,585,612,617,622,629],{"__ignoreMap":54},[58,509,510,512,514,517,519,522,524,527,529,531,533,535,537,539,542,545,547,549],{"class":60,"line":61},[58,511,65],{"class":64},[58,513,69],{"class":68},[58,515,516],{"class":72},"uc ",[58,518,76],{"class":64},[58,520,521],{"class":79},"UseCase",[58,523,83],{"class":68},[58,525,526],{"class":79},"CompleteLesson",[58,528,431],{"class":68},[58,530,248],{"class":72},[58,532,98],{"class":79},[58,534,101],{"class":68},[58,536,104],{"class":79},[58,538,144],{"class":68},[58,540,541],{"class":72},"slug",[58,543,544],{"class":64}," string",[58,546,83],{"class":68},[58,548,147],{"class":64},[58,550,192],{"class":68},[58,552,553,556,558,560,562,564,567,569],{"class":60,"line":92},[58,554,555],{"class":68},"    ctx, cancel ",[58,557,159],{"class":64},[58,559,289],{"class":68},[58,561,292],{"class":79},[58,563,339],{"class":68},[58,565,566],{"class":188},"500",[58,568,76],{"class":64},[58,570,571],{"class":68},"time.Millisecond)\n",[58,573,574,577,579],{"class":60,"line":110},[58,575,576],{"class":64},"    defer",[58,578,317],{"class":79},[58,580,320],{"class":68},[58,582,583],{"class":60,"line":129},[58,584,221],{"emptyLinePlaceholder":220},[58,586,587,590,592,595,598,600,602,605,607,610],{"class":60,"line":153},[58,588,589],{"class":68},"    _, err ",[58,591,159],{"class":64},[58,593,594],{"class":68}," uc.progressClient.",[58,596,597],{"class":79},"MarkCompleted",[58,599,339],{"class":68},[58,601,342],{"class":64},[58,603,604],{"class":79},"progressv1",[58,606,101],{"class":68},[58,608,609],{"class":79},"MarkCompletedRequest",[58,611,351],{"class":68},[58,613,614],{"class":60,"line":176},[58,615,616],{"class":68},"        Slug: slug,\n",[58,618,619],{"class":60,"line":195},[58,620,621],{"class":68},"    })\n",[58,623,624,626],{"class":60,"line":211},[58,625,227],{"class":64},[58,627,628],{"class":68}," err\n",[58,630,631],{"class":60,"line":217},[58,632,242],{"class":68},[15,634,635],{},"Новый timeout ограничивает именно этот вызов, но родительская отмена всё равно сохранится. Если внешний request отменили раньше, дочерний context тоже отменится раньше.",[15,637,638],{},"На сервере полезно логировать deadline для долгих операций:",[49,640,642],{"className":51,"code":641,"language":53,"meta":54,"style":54},"if deadline, ok := ctx.Deadline(); ok {\n    slog.Debug(\"grpc deadline\", \"left\", time.Until(deadline))\n}\n",[19,643,644,662,689],{"__ignoreMap":54},[58,645,646,648,651,653,656,659],{"class":60,"line":61},[58,647,372],{"class":64},[58,649,650],{"class":68}," deadline, ok ",[58,652,159],{"class":64},[58,654,655],{"class":68}," ctx.",[58,657,658],{"class":79},"Deadline",[58,660,661],{"class":68},"(); ok {\n",[58,663,664,667,670,672,675,677,680,683,686],{"class":60,"line":92},[58,665,666],{"class":68},"    slog.",[58,668,669],{"class":79},"Debug",[58,671,431],{"class":68},[58,673,674],{"class":359},"\"grpc deadline\"",[58,676,144],{"class":68},[58,678,679],{"class":359},"\"left\"",[58,681,682],{"class":68},", time.",[58,684,685],{"class":79},"Until",[58,687,688],{"class":68},"(deadline))\n",[58,690,691],{"class":60,"line":110},[58,692,242],{"class":68},[15,694,695],{},"Так легче заметить ситуацию, когда сервер получает запросы уже почти без запаса времени: проблема может быть не в медленном handler, а в слишком коротком клиентском timeout или длинной цепочке upstream-сервисов.",[35,697],{},[38,699,701],{"id":700},"сервер-должен-уважать-отмену","Сервер должен уважать отмену",[15,703,704],{},"Плохой вариант:",[49,706,708],{"className":51,"code":707,"language":53,"meta":54,"style":54},"func (s *Server) SlowOperation(ctx context.Context, req *pb.Request) (*pb.Response, error) {\n    time.Sleep(5 * time.Second)\n    return &pb.Response{Message: \"done\"}, nil\n}\n",[19,709,710,770,788,812],{"__ignoreMap":54},[58,711,712,714,716,718,720,723,725,728,730,732,734,736,738,740,743,745,748,750,753,755,757,759,761,764,766,768],{"class":60,"line":61},[58,713,65],{"class":64},[58,715,69],{"class":68},[58,717,73],{"class":72},[58,719,76],{"class":64},[58,721,722],{"class":79},"Server",[58,724,83],{"class":68},[58,726,727],{"class":79},"SlowOperation",[58,729,431],{"class":68},[58,731,248],{"class":72},[58,733,98],{"class":79},[58,735,101],{"class":68},[58,737,104],{"class":79},[58,739,144],{"class":68},[58,741,742],{"class":72},"req",[58,744,116],{"class":64},[58,746,747],{"class":79},"pb",[58,749,101],{"class":68},[58,751,752],{"class":79},"Request",[58,754,132],{"class":68},[58,756,76],{"class":64},[58,758,747],{"class":79},[58,760,101],{"class":68},[58,762,763],{"class":79},"Response",[58,765,144],{"class":68},[58,767,147],{"class":64},[58,769,150],{"class":68},[58,771,772,775,778,780,783,785],{"class":60,"line":92},[58,773,774],{"class":68},"    time.",[58,776,777],{"class":79},"Sleep",[58,779,431],{"class":68},[58,781,782],{"class":188},"5",[58,784,116],{"class":64},[58,786,787],{"class":68}," time.Second)\n",[58,789,790,792,795,797,799,801,804,807,810],{"class":60,"line":110},[58,791,227],{"class":64},[58,793,794],{"class":64}," &",[58,796,747],{"class":79},[58,798,101],{"class":68},[58,800,763],{"class":79},[58,802,803],{"class":68},"{Message: ",[58,805,806],{"class":359},"\"done\"",[58,808,809],{"class":68},"}, ",[58,811,236],{"class":188},[58,813,814],{"class":60,"line":129},[58,815,242],{"class":68},[15,817,818],{},"Если клиент ушёл через секунду, сервер всё равно спит и держит ресурсы.",[15,820,821],{},"Лучше:",[49,823,825],{"className":51,"code":824,"language":53,"meta":54,"style":54},"func (s *Server) SlowOperation(ctx context.Context, req *pb.Request) (*pb.Response, error) {\n    select {\n    case \u003C-time.After(5 * time.Second):\n        return &pb.Response{Message: \"done\"}, nil\n    case \u003C-ctx.Done():\n        return nil, status.FromContextError(ctx.Err()).Err()\n    }\n}\n",[19,826,827,881,888,911,931,946,971,975],{"__ignoreMap":54},[58,828,829,831,833,835,837,839,841,843,845,847,849,851,853,855,857,859,861,863,865,867,869,871,873,875,877,879],{"class":60,"line":61},[58,830,65],{"class":64},[58,832,69],{"class":68},[58,834,73],{"class":72},[58,836,76],{"class":64},[58,838,722],{"class":79},[58,840,83],{"class":68},[58,842,727],{"class":79},[58,844,431],{"class":68},[58,846,248],{"class":72},[58,848,98],{"class":79},[58,850,101],{"class":68},[58,852,104],{"class":79},[58,854,144],{"class":68},[58,856,742],{"class":72},[58,858,116],{"class":64},[58,860,747],{"class":79},[58,862,101],{"class":68},[58,864,752],{"class":79},[58,866,132],{"class":68},[58,868,76],{"class":64},[58,870,747],{"class":79},[58,872,101],{"class":68},[58,874,763],{"class":79},[58,876,144],{"class":68},[58,878,147],{"class":64},[58,880,150],{"class":68},[58,882,883,886],{"class":60,"line":92},[58,884,885],{"class":64},"    select",[58,887,192],{"class":68},[58,889,890,893,896,899,902,904,906,908],{"class":60,"line":110},[58,891,892],{"class":64},"    case",[58,894,895],{"class":64}," \u003C-",[58,897,898],{"class":68},"time.",[58,900,901],{"class":79},"After",[58,903,431],{"class":68},[58,905,782],{"class":188},[58,907,116],{"class":64},[58,909,910],{"class":68}," time.Second):\n",[58,912,913,915,917,919,921,923,925,927,929],{"class":60,"line":129},[58,914,198],{"class":64},[58,916,794],{"class":64},[58,918,747],{"class":79},[58,920,101],{"class":68},[58,922,763],{"class":79},[58,924,803],{"class":68},[58,926,806],{"class":359},[58,928,809],{"class":68},[58,930,236],{"class":188},[58,932,933,935,937,940,943],{"class":60,"line":153},[58,934,892],{"class":64},[58,936,895],{"class":64},[58,938,939],{"class":68},"ctx.",[58,941,942],{"class":79},"Done",[58,944,945],{"class":68},"():\n",[58,947,948,950,952,955,958,961,964,967,969],{"class":60,"line":176},[58,949,198],{"class":64},[58,951,189],{"class":188},[58,953,954],{"class":68},", status.",[58,956,957],{"class":79},"FromContextError",[58,959,960],{"class":68},"(ctx.",[58,962,963],{"class":79},"Err",[58,965,966],{"class":68},"()).",[58,968,963],{"class":79},[58,970,320],{"class":68},[58,972,973],{"class":60,"line":195},[58,974,214],{"class":68},[58,976,977],{"class":60,"line":211},[58,978,242],{"class":68},[15,980,981,982,985,986,988],{},"В реальном коде вместо ",[19,983,984],{},"time.After"," будут запросы в БД, внешние API, очереди, долгие вычисления. Передавайте ",[19,987,248],{}," дальше, чтобы нижние уровни тоже могли остановиться.",[35,990],{},[38,992,994],{"id":993},"grpc-status-codes","gRPC status codes",[15,996,997,998,101],{},"В REST клиент смотрит на HTTP status code. В gRPC клиент смотрит на ",[19,999,1000],{},"codes.Code",[15,1002,1003],{},"Частые коды:",[1005,1006,1007,1020],"table",{},[1008,1009,1010],"thead",{},[1011,1012,1013,1017],"tr",{},[1014,1015,1016],"th",{},"Код",[1014,1018,1019],{},"Когда использовать",[1021,1022,1023,1034,1044,1054,1064,1074,1084,1094,1104,1114],"tbody",{},[1011,1024,1025,1031],{},[1026,1027,1028],"td",{},[19,1029,1030],{},"InvalidArgument",[1026,1032,1033],{},"request некорректен независимо от состояния системы",[1011,1035,1036,1041],{},[1026,1037,1038],{},[19,1039,1040],{},"NotFound",[1026,1042,1043],{},"сущность не найдена",[1011,1045,1046,1051],{},[1026,1047,1048],{},[19,1049,1050],{},"AlreadyExists",[1026,1052,1053],{},"сущность уже существует",[1011,1055,1056,1061],{},[1026,1057,1058],{},[19,1059,1060],{},"Unauthenticated",[1026,1062,1063],{},"нет или неверные credentials",[1011,1065,1066,1071],{},[1026,1067,1068],{},[19,1069,1070],{},"PermissionDenied",[1026,1072,1073],{},"пользователь известен, но прав не хватает",[1011,1075,1076,1081],{},[1026,1077,1078],{},[19,1079,1080],{},"FailedPrecondition",[1026,1082,1083],{},"операция невозможна в текущем состоянии",[1011,1085,1086,1091],{},[1026,1087,1088],{},[19,1089,1090],{},"Canceled",[1026,1092,1093],{},"операция отменена",[1011,1095,1096,1101],{},[1026,1097,1098],{},[19,1099,1100],{},"DeadlineExceeded",[1026,1102,1103],{},"истёк deadline",[1011,1105,1106,1111],{},[1026,1107,1108],{},[19,1109,1110],{},"Unavailable",[1026,1112,1113],{},"сервис временно недоступен, retry может помочь",[1011,1115,1116,1121],{},[1026,1117,1118],{},[19,1119,1120],{},"Internal",[1026,1122,1123],{},"внутренняя ошибка сервера",[15,1125,1126],{},"Как выбирать код на практике:",[251,1128,1129,1135,1140,1145,1150,1155,1160],{},[254,1130,1131,1132,1134],{},"проблема в форме request, например пустой slug или неверный enum -> ",[19,1133,1030],{},";",[254,1136,1137,1138,1134],{},"request корректный, но нужной сущности нет -> ",[19,1139,1040],{},[254,1141,1142,1143,1134],{},"пользователь не прислал token или token невалиден -> ",[19,1144,1060],{},[254,1146,1147,1148,1134],{},"пользователь распознан, но действие запрещено -> ",[19,1149,1070],{},[254,1151,1152,1153,1134],{},"данные валидны, но операция нарушает текущее состояние, например \"урок уже завершён\" -> ",[19,1154,1080],{},[254,1156,1157,1158,1134],{},"зависимость временно недоступна, и повтор позже может помочь -> ",[19,1159,1110],{},[254,1161,1162,1163,101],{},"сервер сломался неожиданно или получил ошибку, которую нельзя безопасно раскрыть -> ",[19,1164,1120],{},[15,1166,1167,1168,1170],{},"Не надо мапить всё в ",[19,1169,1120],{},". Для клиента это означает: \"я ничего не понял, retry может быть опасен\". Хороший status code превращает ошибку в управляемое поведение: показать validation message, попросить login, не делать retry или, наоборот, попробовать позже.",[15,1172,1173],{},"На сервере:",[49,1175,1177],{"className":51,"code":1176,"language":53,"meta":54,"style":54},"return nil, status.Error(codes.InvalidArgument, \"slug is required\")\n",[19,1178,1179],{"__ignoreMap":54},[58,1180,1181,1184,1186,1188,1191,1194,1197],{"class":60,"line":61},[58,1182,1183],{"class":64},"return",[58,1185,189],{"class":188},[58,1187,954],{"class":68},[58,1189,1190],{"class":79},"Error",[58,1192,1193],{"class":68},"(codes.InvalidArgument, ",[58,1195,1196],{"class":359},"\"slug is required\"",[58,1198,437],{"class":68},[15,1200,1201],{},"На клиенте:",[49,1203,1206],{"className":51,"code":1204,"language":53,"meta":1205,"style":54},"resp, err := client.GetLesson(ctx, req)\nif err != nil {\n    st, ok := status.FromError(err)\n    if !ok {\n        return fmt.Errorf(\"non-grpc error: %w\", err)\n    }\n\n    switch st.Code() {\n    case codes.NotFound:\n        fmt.Println(\"lesson not found\")\n    case codes.DeadlineExceeded:\n        fmt.Println(\"request timed out\")\n    default:\n        fmt.Println(\"grpc error:\", st.Code(), st.Message())\n    }\n    return err\n}\n\n_ = resp\n","no-run",[19,1207,1208,1221,1233,1245,1255,1278,1282,1286,1298,1305,1319,1326,1339,1347,1371,1375,1382,1387,1392],{"__ignoreMap":54},[58,1209,1210,1212,1214,1216,1218],{"class":60,"line":61},[58,1211,329],{"class":68},[58,1213,159],{"class":64},[58,1215,334],{"class":68},[58,1217,86],{"class":79},[58,1219,1220],{"class":68},"(ctx, req)\n",[58,1222,1223,1225,1227,1229,1231],{"class":60,"line":92},[58,1224,372],{"class":64},[58,1226,182],{"class":68},[58,1228,185],{"class":64},[58,1230,189],{"class":188},[58,1232,192],{"class":68},[58,1234,1235,1237,1239,1241,1243],{"class":60,"line":110},[58,1236,385],{"class":68},[58,1238,159],{"class":64},[58,1240,390],{"class":68},[58,1242,393],{"class":79},[58,1244,208],{"class":68},[58,1246,1247,1249,1252],{"class":60,"line":129},[58,1248,179],{"class":64},[58,1250,1251],{"class":64}," !",[58,1253,1254],{"class":68},"ok {\n",[58,1256,1257,1259,1262,1265,1267,1270,1272,1275],{"class":60,"line":153},[58,1258,198],{"class":64},[58,1260,1261],{"class":68}," fmt.",[58,1263,1264],{"class":79},"Errorf",[58,1266,431],{"class":68},[58,1268,1269],{"class":359},"\"non-grpc error: ",[58,1271,32],{"class":188},[58,1273,1274],{"class":359},"\"",[58,1276,1277],{"class":68},", err)\n",[58,1279,1280],{"class":60,"line":176},[58,1281,214],{"class":68},[58,1283,1284],{"class":60,"line":195},[58,1285,221],{"emptyLinePlaceholder":220},[58,1287,1288,1291,1293,1295],{"class":60,"line":211},[58,1289,1290],{"class":64},"    switch",[58,1292,408],{"class":68},[58,1294,411],{"class":79},[58,1296,1297],{"class":68},"() {\n",[58,1299,1300,1302],{"class":60,"line":217},[58,1301,892],{"class":64},[58,1303,1304],{"class":68}," codes.NotFound:\n",[58,1306,1307,1310,1312,1314,1317],{"class":60,"line":224},[58,1308,1309],{"class":68},"        fmt.",[58,1311,428],{"class":79},[58,1313,431],{"class":68},[58,1315,1316],{"class":359},"\"lesson not found\"",[58,1318,437],{"class":68},[58,1320,1321,1323],{"class":60,"line":239},[58,1322,892],{"class":64},[58,1324,1325],{"class":68}," codes.DeadlineExceeded:\n",[58,1327,1328,1330,1332,1334,1337],{"class":60,"line":444},[58,1329,1309],{"class":68},[58,1331,428],{"class":79},[58,1333,431],{"class":68},[58,1335,1336],{"class":359},"\"request timed out\"",[58,1338,437],{"class":68},[58,1340,1341,1344],{"class":60,"line":450},[58,1342,1343],{"class":64},"    default",[58,1345,1346],{"class":68},":\n",[58,1348,1349,1351,1353,1355,1358,1361,1363,1366,1369],{"class":60,"line":455},[58,1350,1309],{"class":68},[58,1352,428],{"class":79},[58,1354,431],{"class":68},[58,1356,1357],{"class":359},"\"grpc error:\"",[58,1359,1360],{"class":68},", st.",[58,1362,411],{"class":79},[58,1364,1365],{"class":68},"(), st.",[58,1367,1368],{"class":79},"Message",[58,1370,173],{"class":68},[58,1372,1373],{"class":60,"line":460},[58,1374,214],{"class":68},[58,1376,1378,1380],{"class":60,"line":1377},16,[58,1379,227],{"class":64},[58,1381,628],{"class":68},[58,1383,1385],{"class":60,"line":1384},17,[58,1386,242],{"class":68},[58,1388,1390],{"class":60,"line":1389},18,[58,1391,221],{"emptyLinePlaceholder":220},[58,1393,1395,1397,1399],{"class":60,"line":1394},19,[58,1396,463],{"class":68},[58,1398,466],{"class":64},[58,1400,469],{"class":68},[35,1402],{},[38,1404,1406],{"id":1405},"interceptor-basics","Interceptor basics",[15,1408,1409,1410,144,1412,1414,1415,1418],{},"Interceptor - это middleware вокруг gRPC handler. Unary interceptor получает ",[19,1411,248],{},[19,1413,742],{},", информацию о методе и функцию ",[19,1416,1417],{},"handler",", которая вызывает следующий interceptor или сам RPC method.",[49,1420,1422],{"className":51,"code":1421,"language":53,"meta":54,"style":54},"func UnaryLoggingInterceptor(\n    ctx context.Context,\n    req any,\n    info *grpc.UnaryServerInfo,\n    handler grpc.UnaryHandler,\n) (any, error) {\n    start := time.Now()\n\n    resp, err := handler(ctx, req)\n\n    slog.Info(\"grpc unary\",\n        \"method\", info.FullMethod,\n        \"code\", status.Code(err).String(),\n        \"duration\", time.Since(start),\n    )\n\n    return resp, err\n}\n",[19,1423,1424,1433,1445,1454,1471,1486,1499,1514,1518,1530,1534,1548,1556,1574,1587,1592,1596,1603],{"__ignoreMap":54},[58,1425,1426,1428,1431],{"class":60,"line":61},[58,1427,65],{"class":64},[58,1429,1430],{"class":79}," UnaryLoggingInterceptor",[58,1432,89],{"class":68},[58,1434,1435,1437,1439,1441,1443],{"class":60,"line":92},[58,1436,95],{"class":72},[58,1438,98],{"class":79},[58,1440,101],{"class":68},[58,1442,104],{"class":79},[58,1444,107],{"class":68},[58,1446,1447,1449,1452],{"class":60,"line":110},[58,1448,113],{"class":72},[58,1450,1451],{"class":79}," any",[58,1453,107],{"class":68},[58,1455,1456,1459,1461,1464,1466,1469],{"class":60,"line":129},[58,1457,1458],{"class":72},"    info",[58,1460,116],{"class":64},[58,1462,1463],{"class":79},"grpc",[58,1465,101],{"class":68},[58,1467,1468],{"class":79},"UnaryServerInfo",[58,1470,107],{"class":68},[58,1472,1473,1476,1479,1481,1484],{"class":60,"line":153},[58,1474,1475],{"class":72},"    handler",[58,1477,1478],{"class":79}," grpc",[58,1480,101],{"class":68},[58,1482,1483],{"class":79},"UnaryHandler",[58,1485,107],{"class":68},[58,1487,1488,1490,1493,1495,1497],{"class":60,"line":176},[58,1489,132],{"class":68},[58,1491,1492],{"class":79},"any",[58,1494,144],{"class":68},[58,1496,147],{"class":64},[58,1498,150],{"class":68},[58,1500,1501,1504,1506,1509,1512],{"class":60,"line":195},[58,1502,1503],{"class":68},"    start ",[58,1505,159],{"class":64},[58,1507,1508],{"class":68}," time.",[58,1510,1511],{"class":79},"Now",[58,1513,320],{"class":68},[58,1515,1516],{"class":60,"line":211},[58,1517,221],{"emptyLinePlaceholder":220},[58,1519,1520,1523,1525,1528],{"class":60,"line":217},[58,1521,1522],{"class":68},"    resp, err ",[58,1524,159],{"class":64},[58,1526,1527],{"class":79}," handler",[58,1529,1220],{"class":68},[58,1531,1532],{"class":60,"line":224},[58,1533,221],{"emptyLinePlaceholder":220},[58,1535,1536,1538,1541,1543,1546],{"class":60,"line":239},[58,1537,666],{"class":68},[58,1539,1540],{"class":79},"Info",[58,1542,431],{"class":68},[58,1544,1545],{"class":359},"\"grpc unary\"",[58,1547,107],{"class":68},[58,1549,1550,1553],{"class":60,"line":444},[58,1551,1552],{"class":359},"        \"method\"",[58,1554,1555],{"class":68},", info.FullMethod,\n",[58,1557,1558,1561,1563,1565,1568,1571],{"class":60,"line":450},[58,1559,1560],{"class":359},"        \"code\"",[58,1562,954],{"class":68},[58,1564,411],{"class":79},[58,1566,1567],{"class":68},"(err).",[58,1569,1570],{"class":79},"String",[58,1572,1573],{"class":68},"(),\n",[58,1575,1576,1579,1581,1584],{"class":60,"line":455},[58,1577,1578],{"class":359},"        \"duration\"",[58,1580,682],{"class":68},[58,1582,1583],{"class":79},"Since",[58,1585,1586],{"class":68},"(start),\n",[58,1588,1589],{"class":60,"line":460},[58,1590,1591],{"class":68},"    )\n",[58,1593,1594],{"class":60,"line":1377},[58,1595,221],{"emptyLinePlaceholder":220},[58,1597,1598,1600],{"class":60,"line":1384},[58,1599,227],{"class":64},[58,1601,1602],{"class":68}," resp, err\n",[58,1604,1605],{"class":60,"line":1389},[58,1606,242],{"class":68},[15,1608,1609],{},"Подключение:",[49,1611,1613],{"className":51,"code":1612,"language":53,"meta":54,"style":54},"grpcServer := grpc.NewServer(\n    grpc.UnaryInterceptor(UnaryLoggingInterceptor),\n)\n",[19,1614,1615,1630,1641],{"__ignoreMap":54},[58,1616,1617,1620,1622,1625,1628],{"class":60,"line":61},[58,1618,1619],{"class":68},"grpcServer ",[58,1621,159],{"class":64},[58,1623,1624],{"class":68}," grpc.",[58,1626,1627],{"class":79},"NewServer",[58,1629,89],{"class":68},[58,1631,1632,1635,1638],{"class":60,"line":92},[58,1633,1634],{"class":68},"    grpc.",[58,1636,1637],{"class":79},"UnaryInterceptor",[58,1639,1640],{"class":68},"(UnaryLoggingInterceptor),\n",[58,1642,1643],{"class":60,"line":110},[58,1644,437],{"class":68},[15,1646,1647],{},"Если нужно несколько interceptor'ов, в grpc-go используют chain:",[49,1649,1651],{"className":51,"code":1650,"language":53,"meta":54,"style":54},"grpcServer := grpc.NewServer(\n    grpc.ChainUnaryInterceptor(\n        RequestIDInterceptor,\n        AuthInterceptor,\n        UnaryLoggingInterceptor,\n        RecoveryInterceptor,\n    ),\n)\n",[19,1652,1653,1665,1674,1679,1684,1689,1694,1699],{"__ignoreMap":54},[58,1654,1655,1657,1659,1661,1663],{"class":60,"line":61},[58,1656,1619],{"class":68},[58,1658,159],{"class":64},[58,1660,1624],{"class":68},[58,1662,1627],{"class":79},[58,1664,89],{"class":68},[58,1666,1667,1669,1672],{"class":60,"line":92},[58,1668,1634],{"class":68},[58,1670,1671],{"class":79},"ChainUnaryInterceptor",[58,1673,89],{"class":68},[58,1675,1676],{"class":60,"line":110},[58,1677,1678],{"class":68},"        RequestIDInterceptor,\n",[58,1680,1681],{"class":60,"line":129},[58,1682,1683],{"class":68},"        AuthInterceptor,\n",[58,1685,1686],{"class":60,"line":153},[58,1687,1688],{"class":68},"        UnaryLoggingInterceptor,\n",[58,1690,1691],{"class":60,"line":176},[58,1692,1693],{"class":68},"        RecoveryInterceptor,\n",[58,1695,1696],{"class":60,"line":195},[58,1697,1698],{"class":68},"    ),\n",[58,1700,1701],{"class":60,"line":211},[58,1702,437],{"class":68},[15,1704,1705,1706,1709],{},"Порядок важен. Request id и auth часто должны отработать до handler, logging должен увидеть итоговый status code, recovery должен превратить panic в ",[19,1707,1708],{},"codes.Internal",", а не дать процессу упасть.",[15,1711,1712],{},"Что нормально делать в interceptors:",[251,1714,1715,1718,1721,1724,1727],{},[254,1716,1717],{},"проставить request id в context;",[254,1719,1720],{},"проверить auth metadata;",[254,1722,1723],{},"записать duration\u002Fcode в metrics;",[254,1725,1726],{},"добавить tracing span;",[254,1728,1729],{},"восстановиться после panic.",[15,1731,1732],{},"Что не надо делать:",[251,1734,1735,1738,1741,1744],{},[254,1736,1737],{},"писать бизнес-правила;",[254,1739,1740],{},"менять смысл request payload;",[254,1742,1743],{},"ходить в БД за доменными решениями;",[254,1745,1746],{},"логировать весь request\u002Fresponse с персональными данными.",[15,1748,1749,1750,1753,1754,1757],{},"Для streaming RPC нужен отдельный stream interceptor. Unary interceptor не оборачивает ",[19,1751,1752],{},"Send","\u002F",[19,1755,1756],{},"Recv"," внутри stream.",[35,1759],{},[38,1761,1763],{"id":1762},"domain-errors-не-должны-знать-про-grpc","Domain errors не должны знать про gRPC",[15,1765,1766,1767,1770],{},"В Clean Architecture usecase не должен импортировать ",[19,1768,1769],{},"google.golang.org\u002Fgrpc\u002Fstatus",". Домен и application layer возвращают свои ошибки:",[49,1772,1774],{"className":51,"code":1773,"language":53,"meta":54,"style":54},"package domain\n\nimport \"errors\"\n\nvar (\n    ErrLessonNotFound = errors.New(\"lesson not found\")\n    ErrAlreadyDone    = errors.New(\"lesson already done\")\n)\n\ntype ValidationError struct {\n    Field   string\n    Message string\n}\n\nfunc (e ValidationError) Error() string {\n    return e.Field + \": \" + e.Message\n}\n",[19,1775,1776,1784,1788,1802,1806,1814,1833,1851,1855,1859,1872,1880,1887,1891,1895,1918,1937],{"__ignoreMap":54},[58,1777,1778,1781],{"class":60,"line":61},[58,1779,1780],{"class":64},"package",[58,1782,1783],{"class":79}," domain\n",[58,1785,1786],{"class":60,"line":92},[58,1787,221],{"emptyLinePlaceholder":220},[58,1789,1790,1793,1796,1799],{"class":60,"line":110},[58,1791,1792],{"class":64},"import",[58,1794,1795],{"class":359}," \"",[58,1797,1798],{"class":79},"errors",[58,1800,1801],{"class":359},"\"\n",[58,1803,1804],{"class":60,"line":129},[58,1805,221],{"emptyLinePlaceholder":220},[58,1807,1808,1811],{"class":60,"line":153},[58,1809,1810],{"class":64},"var",[58,1812,1813],{"class":68}," (\n",[58,1815,1816,1819,1821,1824,1827,1829,1831],{"class":60,"line":176},[58,1817,1818],{"class":68},"    ErrLessonNotFound ",[58,1820,466],{"class":64},[58,1822,1823],{"class":68}," errors.",[58,1825,1826],{"class":79},"New",[58,1828,431],{"class":68},[58,1830,1316],{"class":359},[58,1832,437],{"class":68},[58,1834,1835,1838,1840,1842,1844,1846,1849],{"class":60,"line":195},[58,1836,1837],{"class":68},"    ErrAlreadyDone    ",[58,1839,466],{"class":64},[58,1841,1823],{"class":68},[58,1843,1826],{"class":79},[58,1845,431],{"class":68},[58,1847,1848],{"class":359},"\"lesson already done\"",[58,1850,437],{"class":68},[58,1852,1853],{"class":60,"line":211},[58,1854,437],{"class":68},[58,1856,1857],{"class":60,"line":217},[58,1858,221],{"emptyLinePlaceholder":220},[58,1860,1861,1864,1867,1870],{"class":60,"line":224},[58,1862,1863],{"class":64},"type",[58,1865,1866],{"class":79}," ValidationError",[58,1868,1869],{"class":64}," struct",[58,1871,192],{"class":68},[58,1873,1874,1877],{"class":60,"line":239},[58,1875,1876],{"class":68},"    Field   ",[58,1878,1879],{"class":64},"string\n",[58,1881,1882,1885],{"class":60,"line":444},[58,1883,1884],{"class":68},"    Message ",[58,1886,1879],{"class":64},[58,1888,1889],{"class":60,"line":450},[58,1890,242],{"class":68},[58,1892,1893],{"class":60,"line":455},[58,1894,221],{"emptyLinePlaceholder":220},[58,1896,1897,1899,1901,1904,1907,1909,1911,1913,1916],{"class":60,"line":460},[58,1898,65],{"class":64},[58,1900,69],{"class":68},[58,1902,1903],{"class":72},"e ",[58,1905,1906],{"class":79},"ValidationError",[58,1908,83],{"class":68},[58,1910,1190],{"class":79},[58,1912,414],{"class":68},[58,1914,1915],{"class":64},"string",[58,1917,192],{"class":68},[58,1919,1920,1922,1925,1928,1931,1934],{"class":60,"line":1377},[58,1921,227],{"class":64},[58,1923,1924],{"class":68}," e.Field ",[58,1926,1927],{"class":64},"+",[58,1929,1930],{"class":359}," \": \"",[58,1932,1933],{"class":64}," +",[58,1935,1936],{"class":68}," e.Message\n",[58,1938,1939],{"class":60,"line":1384},[58,1940,242],{"class":68},[15,1942,1943],{},"Infrastructure или usecase может оборачивать ошибку:",[49,1945,1947],{"className":51,"code":1946,"language":53,"meta":54,"style":54},"if err := repo.Save(ctx, progress); err != nil {\n    return fmt.Errorf(\"save progress: %w\", err)\n}\n",[19,1948,1949,1972,1991],{"__ignoreMap":54},[58,1950,1951,1953,1955,1957,1960,1963,1966,1968,1970],{"class":60,"line":61},[58,1952,372],{"class":64},[58,1954,182],{"class":68},[58,1956,159],{"class":64},[58,1958,1959],{"class":68}," repo.",[58,1961,1962],{"class":79},"Save",[58,1964,1965],{"class":68},"(ctx, progress); err ",[58,1967,185],{"class":64},[58,1969,189],{"class":188},[58,1971,192],{"class":68},[58,1973,1974,1976,1978,1980,1982,1985,1987,1989],{"class":60,"line":92},[58,1975,227],{"class":64},[58,1977,1261],{"class":68},[58,1979,1264],{"class":79},[58,1981,431],{"class":68},[58,1983,1984],{"class":359},"\"save progress: ",[58,1986,32],{"class":188},[58,1988,1274],{"class":359},[58,1990,1277],{"class":68},[58,1992,1993],{"class":60,"line":110},[58,1994,242],{"class":68},[15,1996,1997],{},"gRPC adapter мапит доменные ошибки в transport codes:",[49,1999,2001],{"className":51,"code":2000,"language":53,"meta":54,"style":54},"func toGRPCError(err error) error {\n    if err == nil {\n        return nil\n    }\n\n    switch {\n    case errors.Is(err, domain.ErrLessonNotFound):\n        return status.Error(codes.NotFound, \"lesson not found\")\n    case errors.Is(err, domain.ErrAlreadyDone):\n        return status.Error(codes.FailedPrecondition, \"lesson already completed\")\n    case errors.Is(err, context.Canceled):\n        return status.Error(codes.Canceled, \"request canceled\")\n    case errors.Is(err, context.DeadlineExceeded):\n        return status.Error(codes.DeadlineExceeded, \"deadline exceeded\")\n    }\n\n    var validationErr domain.ValidationError\n    if errors.As(err, &validationErr) {\n        return status.Error(codes.InvalidArgument, validationErr.Error())\n    }\n\n    return status.Error(codes.Internal, \"internal error\")\n}\n",[19,2002,2003,2024,2036,2043,2047,2051,2057,2069,2084,2095,2111,2122,2138,2149,2165,2169,2173,2189,2206,2221,2226,2231,2248],{"__ignoreMap":54},[58,2004,2005,2007,2010,2012,2015,2018,2020,2022],{"class":60,"line":61},[58,2006,65],{"class":64},[58,2008,2009],{"class":79}," toGRPCError",[58,2011,431],{"class":68},[58,2013,2014],{"class":72},"err",[58,2016,2017],{"class":64}," error",[58,2019,83],{"class":68},[58,2021,147],{"class":64},[58,2023,192],{"class":68},[58,2025,2026,2028,2030,2032,2034],{"class":60,"line":92},[58,2027,179],{"class":64},[58,2029,182],{"class":68},[58,2031,417],{"class":64},[58,2033,189],{"class":188},[58,2035,192],{"class":68},[58,2037,2038,2040],{"class":60,"line":110},[58,2039,198],{"class":64},[58,2041,2042],{"class":188}," nil\n",[58,2044,2045],{"class":60,"line":129},[58,2046,214],{"class":68},[58,2048,2049],{"class":60,"line":153},[58,2050,221],{"emptyLinePlaceholder":220},[58,2052,2053,2055],{"class":60,"line":176},[58,2054,1290],{"class":64},[58,2056,192],{"class":68},[58,2058,2059,2061,2063,2066],{"class":60,"line":195},[58,2060,892],{"class":64},[58,2062,1823],{"class":68},[58,2064,2065],{"class":79},"Is",[58,2067,2068],{"class":68},"(err, domain.ErrLessonNotFound):\n",[58,2070,2071,2073,2075,2077,2080,2082],{"class":60,"line":211},[58,2072,198],{"class":64},[58,2074,390],{"class":68},[58,2076,1190],{"class":79},[58,2078,2079],{"class":68},"(codes.NotFound, ",[58,2081,1316],{"class":359},[58,2083,437],{"class":68},[58,2085,2086,2088,2090,2092],{"class":60,"line":217},[58,2087,892],{"class":64},[58,2089,1823],{"class":68},[58,2091,2065],{"class":79},[58,2093,2094],{"class":68},"(err, domain.ErrAlreadyDone):\n",[58,2096,2097,2099,2101,2103,2106,2109],{"class":60,"line":224},[58,2098,198],{"class":64},[58,2100,390],{"class":68},[58,2102,1190],{"class":79},[58,2104,2105],{"class":68},"(codes.FailedPrecondition, ",[58,2107,2108],{"class":359},"\"lesson already completed\"",[58,2110,437],{"class":68},[58,2112,2113,2115,2117,2119],{"class":60,"line":239},[58,2114,892],{"class":64},[58,2116,1823],{"class":68},[58,2118,2065],{"class":79},[58,2120,2121],{"class":68},"(err, context.Canceled):\n",[58,2123,2124,2126,2128,2130,2133,2136],{"class":60,"line":444},[58,2125,198],{"class":64},[58,2127,390],{"class":68},[58,2129,1190],{"class":79},[58,2131,2132],{"class":68},"(codes.Canceled, ",[58,2134,2135],{"class":359},"\"request canceled\"",[58,2137,437],{"class":68},[58,2139,2140,2142,2144,2146],{"class":60,"line":450},[58,2141,892],{"class":64},[58,2143,1823],{"class":68},[58,2145,2065],{"class":79},[58,2147,2148],{"class":68},"(err, context.DeadlineExceeded):\n",[58,2150,2151,2153,2155,2157,2160,2163],{"class":60,"line":455},[58,2152,198],{"class":64},[58,2154,390],{"class":68},[58,2156,1190],{"class":79},[58,2158,2159],{"class":68},"(codes.DeadlineExceeded, ",[58,2161,2162],{"class":359},"\"deadline exceeded\"",[58,2164,437],{"class":68},[58,2166,2167],{"class":60,"line":460},[58,2168,214],{"class":68},[58,2170,2171],{"class":60,"line":1377},[58,2172,221],{"emptyLinePlaceholder":220},[58,2174,2175,2178,2181,2184,2186],{"class":60,"line":1384},[58,2176,2177],{"class":64},"    var",[58,2179,2180],{"class":68}," validationErr ",[58,2182,2183],{"class":79},"domain",[58,2185,101],{"class":68},[58,2187,2188],{"class":79},"ValidationError\n",[58,2190,2191,2193,2195,2198,2201,2203],{"class":60,"line":1389},[58,2192,179],{"class":64},[58,2194,1823],{"class":68},[58,2196,2197],{"class":79},"As",[58,2199,2200],{"class":68},"(err, ",[58,2202,342],{"class":64},[58,2204,2205],{"class":68},"validationErr) {\n",[58,2207,2208,2210,2212,2214,2217,2219],{"class":60,"line":1394},[58,2209,198],{"class":64},[58,2211,390],{"class":68},[58,2213,1190],{"class":79},[58,2215,2216],{"class":68},"(codes.InvalidArgument, validationErr.",[58,2218,1190],{"class":79},[58,2220,173],{"class":68},[58,2222,2224],{"class":60,"line":2223},20,[58,2225,214],{"class":68},[58,2227,2229],{"class":60,"line":2228},21,[58,2230,221],{"emptyLinePlaceholder":220},[58,2232,2234,2236,2238,2240,2243,2246],{"class":60,"line":2233},22,[58,2235,227],{"class":64},[58,2237,390],{"class":68},[58,2239,1190],{"class":79},[58,2241,2242],{"class":68},"(codes.Internal, ",[58,2244,2245],{"class":359},"\"internal error\"",[58,2247,437],{"class":68},[58,2249,2251],{"class":60,"line":2250},23,[58,2252,242],{"class":68},[15,2254,2255],{},"Клиенту не нужно знать, что внутри была PostgreSQL, Redis или файловая система. Внешний контракт - gRPC code и безопасное сообщение.",[35,2257],{},[38,2259,2261],{"id":2260},"metadata","Metadata",[15,2263,2264],{},"Metadata - аналог headers в модели gRPC. Её используют для токенов, request id, trace id, language, tenant id.",[15,2266,2267],{},"Клиент:",[49,2269,2271],{"className":51,"code":2270,"language":53,"meta":54,"style":54},"ctx := metadata.AppendToOutgoingContext(\n    context.Background(),\n    \"authorization\", \"Bearer token\",\n    \"x-request-id\", \"req-123\",\n)\n\nresp, err := client.GetLesson(ctx, req)\n",[19,2272,2273,2288,2297,2309,2321,2325,2329],{"__ignoreMap":54},[58,2274,2275,2278,2280,2283,2286],{"class":60,"line":61},[58,2276,2277],{"class":68},"ctx ",[58,2279,159],{"class":64},[58,2281,2282],{"class":68}," metadata.",[58,2284,2285],{"class":79},"AppendToOutgoingContext",[58,2287,89],{"class":68},[58,2289,2290,2293,2295],{"class":60,"line":92},[58,2291,2292],{"class":68},"    context.",[58,2294,298],{"class":79},[58,2296,1573],{"class":68},[58,2298,2299,2302,2304,2307],{"class":60,"line":110},[58,2300,2301],{"class":359},"    \"authorization\"",[58,2303,144],{"class":68},[58,2305,2306],{"class":359},"\"Bearer token\"",[58,2308,107],{"class":68},[58,2310,2311,2314,2316,2319],{"class":60,"line":129},[58,2312,2313],{"class":359},"    \"x-request-id\"",[58,2315,144],{"class":68},[58,2317,2318],{"class":359},"\"req-123\"",[58,2320,107],{"class":68},[58,2322,2323],{"class":60,"line":153},[58,2324,437],{"class":68},[58,2326,2327],{"class":60,"line":176},[58,2328,221],{"emptyLinePlaceholder":220},[58,2330,2331,2333,2335,2337,2339],{"class":60,"line":195},[58,2332,329],{"class":68},[58,2334,159],{"class":64},[58,2336,334],{"class":68},[58,2338,86],{"class":79},[58,2340,1220],{"class":68},[15,2342,2343],{},"Сервер:",[49,2345,2347],{"className":51,"code":2346,"language":53,"meta":54,"style":54},"func requestIDFromContext(ctx context.Context) string {\n    md, ok := metadata.FromIncomingContext(ctx)\n    if !ok {\n        return \"\"\n    }\n\n    ids := md.Get(\"x-request-id\")\n    if len(ids) == 0 {\n        return \"\"\n    }\n\n    return ids[0]\n}\n",[19,2348,2349,2372,2387,2395,2402,2406,2410,2430,2447,2453,2457,2461,2474],{"__ignoreMap":54},[58,2350,2351,2353,2356,2358,2360,2362,2364,2366,2368,2370],{"class":60,"line":61},[58,2352,65],{"class":64},[58,2354,2355],{"class":79}," requestIDFromContext",[58,2357,431],{"class":68},[58,2359,248],{"class":72},[58,2361,98],{"class":79},[58,2363,101],{"class":68},[58,2365,104],{"class":79},[58,2367,83],{"class":68},[58,2369,1915],{"class":64},[58,2371,192],{"class":68},[58,2373,2374,2377,2379,2381,2384],{"class":60,"line":92},[58,2375,2376],{"class":68},"    md, ok ",[58,2378,159],{"class":64},[58,2380,2282],{"class":68},[58,2382,2383],{"class":79},"FromIncomingContext",[58,2385,2386],{"class":68},"(ctx)\n",[58,2388,2389,2391,2393],{"class":60,"line":110},[58,2390,179],{"class":64},[58,2392,1251],{"class":64},[58,2394,1254],{"class":68},[58,2396,2397,2399],{"class":60,"line":129},[58,2398,198],{"class":64},[58,2400,2401],{"class":359}," \"\"\n",[58,2403,2404],{"class":60,"line":153},[58,2405,214],{"class":68},[58,2407,2408],{"class":60,"line":176},[58,2409,221],{"emptyLinePlaceholder":220},[58,2411,2412,2415,2417,2420,2423,2425,2428],{"class":60,"line":195},[58,2413,2414],{"class":68},"    ids ",[58,2416,159],{"class":64},[58,2418,2419],{"class":68}," md.",[58,2421,2422],{"class":79},"Get",[58,2424,431],{"class":68},[58,2426,2427],{"class":359},"\"x-request-id\"",[58,2429,437],{"class":68},[58,2431,2432,2434,2437,2440,2442,2445],{"class":60,"line":211},[58,2433,179],{"class":64},[58,2435,2436],{"class":79}," len",[58,2438,2439],{"class":68},"(ids) ",[58,2441,417],{"class":64},[58,2443,2444],{"class":188}," 0",[58,2446,192],{"class":68},[58,2448,2449,2451],{"class":60,"line":217},[58,2450,198],{"class":64},[58,2452,2401],{"class":359},[58,2454,2455],{"class":60,"line":224},[58,2456,214],{"class":68},[58,2458,2459],{"class":60,"line":239},[58,2460,221],{"emptyLinePlaceholder":220},[58,2462,2463,2465,2468,2471],{"class":60,"line":444},[58,2464,227],{"class":64},[58,2466,2467],{"class":68}," ids[",[58,2469,2470],{"class":188},"0",[58,2472,2473],{"class":68},"]\n",[58,2475,2476],{"class":60,"line":450},[58,2477,242],{"class":68},[15,2479,2480],{},"Не кладите в metadata большие payload. Это служебная информация, а не body.",[35,2482],{},[38,2484,2486],{"id":2485},"практика","Практика",[15,2488,2489,2490,47],{},"Напишите ",[19,2491,2492],{},"toGRPCError(err error) error",[251,2494,2495,2504,2512,2520,2528,2535],{},[254,2496,2497,2500,2501,1134],{},[19,2498,2499],{},"domain.ErrLessonNotFound"," -> ",[19,2502,2503],{},"codes.NotFound",[254,2505,2506,2500,2509,1134],{},[19,2507,2508],{},"domain.ErrAlreadyDone",[19,2510,2511],{},"codes.FailedPrecondition",[254,2513,2514,2500,2517,1134],{},[19,2515,2516],{},"domain.ValidationError",[19,2518,2519],{},"codes.InvalidArgument",[254,2521,2522,2500,2525,1134],{},[19,2523,2524],{},"context.Canceled",[19,2526,2527],{},"codes.Canceled",[254,2529,2530,2500,2533,1134],{},[19,2531,2532],{},"context.DeadlineExceeded",[19,2534,475],{},[254,2536,2537,2538,101],{},"всё остальное -> ",[19,2539,1708],{},[15,2541,2542],{},"Потом напишите клиентскую обработку:",[251,2544,2545,2550,2555,2560],{},[254,2546,2547,2549],{},[19,2548,1040],{}," показывает \"урок не найден\";",[254,2551,2552,2554],{},[19,2553,1100],{}," предлагает повторить позже;",[254,2556,2557,2559],{},[19,2558,1110],{}," включает retry;",[254,2561,2562,2564],{},[19,2563,1120],{}," не показывает внутренний текст ошибки пользователю.",[35,2566],{},[38,2568,2570],{"id":2569},"типичные-ошибки","Типичные ошибки",[251,2572,2573,2580,2585,2592,2601,2608,2611],{},[254,2574,2575,2576,2579],{},"Возвращать наружу ",[19,2577,2578],{},"err.Error()"," от базы данных.",[254,2581,2582,2583,101],{},"Мапить все ошибки в ",[19,2584,1120],{},[254,2586,2587,2588,29,2590,101],{},"Путать ",[19,2589,1060],{},[19,2591,1070],{},[254,2593,2594,2595,2598,2599,101],{},"Терять wrapping через ",[19,2596,2597],{},"fmt.Errorf(\"x: %v\", err)"," вместо ",[19,2600,32],{},[254,2602,2603,2604,2607],{},"Тащить ",[19,2605,2606],{},"status.Error"," внутрь domain\u002Fusecase слоя.",[254,2609,2610],{},"Не ставить deadline на клиенте.",[254,2612,2613],{},"Не проверять cancellation в долгих серверных операциях.",[35,2615],{},[38,2617,2619],{"id":2618},"источники","Источники",[251,2621,2622,2631,2638,2645,2652,2659,2665,2672],{},[254,2623,2624],{},[2625,2626,2630],"a",{"href":2627,"rel":2628},"https:\u002F\u002Fgrpc.io\u002Fdocs\u002Fguides\u002Fstatus-codes\u002F",[2629],"nofollow","gRPC Status Codes",[254,2632,2633],{},[2625,2634,2637],{"href":2635,"rel":2636},"https:\u002F\u002Fgrpc.io\u002Fdocs\u002Fguides\u002Ferror\u002F",[2629],"gRPC Error Handling",[254,2639,2640],{},[2625,2641,2644],{"href":2642,"rel":2643},"https:\u002F\u002Fgrpc.io\u002Fdocs\u002Fguides\u002Fdeadlines\u002F",[2629],"gRPC Deadlines",[254,2646,2647],{},[2625,2648,2651],{"href":2649,"rel":2650},"https:\u002F\u002Fgrpc.io\u002Fdocs\u002Fguides\u002Fcancellation\u002F",[2629],"gRPC Cancellation",[254,2653,2654],{},[2625,2655,2658],{"href":2656,"rel":2657},"https:\u002F\u002Fgrpc.io\u002Fdocs\u002Fguides\u002Fmetadata\u002F",[2629],"gRPC Metadata",[254,2660,2661],{},[2625,2662,1769],{"href":2663,"rel":2664},"https:\u002F\u002Fpkg.go.dev\u002Fgoogle.golang.org\u002Fgrpc\u002Fstatus",[2629],[254,2666,2667],{},[2625,2668,2671],{"href":2669,"rel":2670},"https:\u002F\u002Fgo.dev\u002Fblog\u002Fgo1.13-errors",[2629],"Go blog: Working with Errors in Go 1.13",[254,2673,2674],{},[2625,2675,2678],{"href":2676,"rel":2677},"https:\u002F\u002Fgo.dev\u002Fblog\u002Fcontext",[2629],"Go blog: Context",[35,2680],{},[38,2682,2684],{"id":2683},"интерактивная-практика","Интерактивная практика",[2686,2687,2691,2694,2715],"quiz",{"answer":2688,"id":2689,"xp":2690},"4","rpc-errors-q1","10",[15,2692,2693],{},"Какой gRPC status code лучше вернуть, если клиент передал невалидный аргумент?",[2695,2696,2697],"template",{"v-slot:options":54},[251,2698,2699,2703,2707,2711],{},[254,2700,2701],{},[19,2702,1120],{},[254,2704,2705],{},[19,2706,1110],{},[254,2708,2709],{},[19,2710,1070],{},[254,2712,2713],{},[19,2714,1030],{},[2695,2716,2717],{"v-slot:explanation":54},[15,2718,2719,2720,2722],{},"Невалидные входные данные — ошибка клиента. ",[19,2721,1120],{}," оставляют для неожиданных внутренних отказов.",[2724,2725,2729,2732,2916],"predict",{"answer":2726,"id":2727,"xp":2728},"DeadlineExceeded\\nCanceled\\nInternal","rpc-errors-p1","15",[15,2730,2731],{},"Что выведет программа?",[2695,2733,2734],{"v-slot:code":54},[49,2735,2737],{"className":51,"code":2736,"language":53,"meta":54,"style":54},"package main\n\nimport \"fmt\"\n\nfunc transportError(kind string) string {\n    switch kind {\n    case \"timeout\":\n        return \"DeadlineExceeded\"\n    case \"client_cancel\":\n        return \"Canceled\"\n    default:\n        return \"Internal\"\n    }\n}\n\nfunc main() {\n    fmt.Println(transportError(\"timeout\"))\n    fmt.Println(transportError(\"client_cancel\"))\n    fmt.Println(transportError(\"panic\"))\n}\n",[19,2738,2739,2746,2750,2761,2765,2785,2792,2801,2808,2817,2824,2830,2837,2841,2845,2849,2858,2878,2895,2912],{"__ignoreMap":54},[58,2740,2741,2743],{"class":60,"line":61},[58,2742,1780],{"class":64},[58,2744,2745],{"class":79}," main\n",[58,2747,2748],{"class":60,"line":92},[58,2749,221],{"emptyLinePlaceholder":220},[58,2751,2752,2754,2756,2759],{"class":60,"line":110},[58,2753,1792],{"class":64},[58,2755,1795],{"class":359},[58,2757,2758],{"class":79},"fmt",[58,2760,1801],{"class":359},[58,2762,2763],{"class":60,"line":129},[58,2764,221],{"emptyLinePlaceholder":220},[58,2766,2767,2769,2772,2774,2777,2779,2781,2783],{"class":60,"line":153},[58,2768,65],{"class":64},[58,2770,2771],{"class":79}," transportError",[58,2773,431],{"class":68},[58,2775,2776],{"class":72},"kind",[58,2778,544],{"class":64},[58,2780,83],{"class":68},[58,2782,1915],{"class":64},[58,2784,192],{"class":68},[58,2786,2787,2789],{"class":60,"line":176},[58,2788,1290],{"class":64},[58,2790,2791],{"class":68}," kind {\n",[58,2793,2794,2796,2799],{"class":60,"line":195},[58,2795,892],{"class":64},[58,2797,2798],{"class":359}," \"timeout\"",[58,2800,1346],{"class":68},[58,2802,2803,2805],{"class":60,"line":211},[58,2804,198],{"class":64},[58,2806,2807],{"class":359}," \"DeadlineExceeded\"\n",[58,2809,2810,2812,2815],{"class":60,"line":217},[58,2811,892],{"class":64},[58,2813,2814],{"class":359}," \"client_cancel\"",[58,2816,1346],{"class":68},[58,2818,2819,2821],{"class":60,"line":224},[58,2820,198],{"class":64},[58,2822,2823],{"class":359}," \"Canceled\"\n",[58,2825,2826,2828],{"class":60,"line":239},[58,2827,1343],{"class":64},[58,2829,1346],{"class":68},[58,2831,2832,2834],{"class":60,"line":444},[58,2833,198],{"class":64},[58,2835,2836],{"class":359}," \"Internal\"\n",[58,2838,2839],{"class":60,"line":450},[58,2840,214],{"class":68},[58,2842,2843],{"class":60,"line":455},[58,2844,242],{"class":68},[58,2846,2847],{"class":60,"line":460},[58,2848,221],{"emptyLinePlaceholder":220},[58,2850,2851,2853,2856],{"class":60,"line":1377},[58,2852,65],{"class":64},[58,2854,2855],{"class":79}," main",[58,2857,1297],{"class":68},[58,2859,2860,2863,2865,2867,2870,2872,2875],{"class":60,"line":1384},[58,2861,2862],{"class":68},"    fmt.",[58,2864,428],{"class":79},[58,2866,431],{"class":68},[58,2868,2869],{"class":79},"transportError",[58,2871,431],{"class":68},[58,2873,2874],{"class":359},"\"timeout\"",[58,2876,2877],{"class":68},"))\n",[58,2879,2880,2882,2884,2886,2888,2890,2893],{"class":60,"line":1389},[58,2881,2862],{"class":68},[58,2883,428],{"class":79},[58,2885,431],{"class":68},[58,2887,2869],{"class":79},[58,2889,431],{"class":68},[58,2891,2892],{"class":359},"\"client_cancel\"",[58,2894,2877],{"class":68},[58,2896,2897,2899,2901,2903,2905,2907,2910],{"class":60,"line":1394},[58,2898,2862],{"class":68},[58,2900,428],{"class":79},[58,2902,431],{"class":68},[58,2904,2869],{"class":79},[58,2906,431],{"class":68},[58,2908,2909],{"class":359},"\"panic\"",[58,2911,2877],{"class":68},[58,2913,2914],{"class":60,"line":2223},[58,2915,242],{"class":68},[2695,2917,2918],{"v-slot:hint":54},[15,2919,2920],{},"Timeout и cancellation — разные причины завершения RPC, и клиенту важно их различать.",[2922,2923,2927,2934,3061],"code-task",{"expected":2924,"id":2925,"xp":2926},"NotFound\\nInvalidArgument\\nInternal","rpc-errors-ct1","20",[15,2928,2929,2930,2933],{},"Реализуй ",[19,2931,2932],{},"GRPCCode",": сопоставь доменную ошибку с названием gRPC-кода.",[2695,2935,2936],{"v-slot:template":54},[49,2937,2939],{"className":51,"code":2938,"language":53,"meta":54,"style":54},"package main\n\nimport \"fmt\"\n\nfunc GRPCCode(kind string) string {\n    return \"Internal\"\n}\n\nfunc main() {\n    fmt.Println(GRPCCode(\"not_found\"))\n    fmt.Println(GRPCCode(\"bad_request\"))\n    fmt.Println(GRPCCode(\"db_down\"))\n}\n",[19,2940,2941,2947,2951,2961,2965,2984,2990,2994,2998,3006,3023,3040,3057],{"__ignoreMap":54},[58,2942,2943,2945],{"class":60,"line":61},[58,2944,1780],{"class":64},[58,2946,2745],{"class":79},[58,2948,2949],{"class":60,"line":92},[58,2950,221],{"emptyLinePlaceholder":220},[58,2952,2953,2955,2957,2959],{"class":60,"line":110},[58,2954,1792],{"class":64},[58,2956,1795],{"class":359},[58,2958,2758],{"class":79},[58,2960,1801],{"class":359},[58,2962,2963],{"class":60,"line":129},[58,2964,221],{"emptyLinePlaceholder":220},[58,2966,2967,2969,2972,2974,2976,2978,2980,2982],{"class":60,"line":153},[58,2968,65],{"class":64},[58,2970,2971],{"class":79}," GRPCCode",[58,2973,431],{"class":68},[58,2975,2776],{"class":72},[58,2977,544],{"class":64},[58,2979,83],{"class":68},[58,2981,1915],{"class":64},[58,2983,192],{"class":68},[58,2985,2986,2988],{"class":60,"line":176},[58,2987,227],{"class":64},[58,2989,2836],{"class":359},[58,2991,2992],{"class":60,"line":195},[58,2993,242],{"class":68},[58,2995,2996],{"class":60,"line":211},[58,2997,221],{"emptyLinePlaceholder":220},[58,2999,3000,3002,3004],{"class":60,"line":217},[58,3001,65],{"class":64},[58,3003,2855],{"class":79},[58,3005,1297],{"class":68},[58,3007,3008,3010,3012,3014,3016,3018,3021],{"class":60,"line":224},[58,3009,2862],{"class":68},[58,3011,428],{"class":79},[58,3013,431],{"class":68},[58,3015,2932],{"class":79},[58,3017,431],{"class":68},[58,3019,3020],{"class":359},"\"not_found\"",[58,3022,2877],{"class":68},[58,3024,3025,3027,3029,3031,3033,3035,3038],{"class":60,"line":239},[58,3026,2862],{"class":68},[58,3028,428],{"class":79},[58,3030,431],{"class":68},[58,3032,2932],{"class":79},[58,3034,431],{"class":68},[58,3036,3037],{"class":359},"\"bad_request\"",[58,3039,2877],{"class":68},[58,3041,3042,3044,3046,3048,3050,3052,3055],{"class":60,"line":444},[58,3043,2862],{"class":68},[58,3045,428],{"class":79},[58,3047,431],{"class":68},[58,3049,2932],{"class":79},[58,3051,431],{"class":68},[58,3053,3054],{"class":359},"\"db_down\"",[58,3056,2877],{"class":68},[58,3058,3059],{"class":60,"line":450},[58,3060,242],{"class":68},[2695,3062,3063],{"v-slot:hints":54},[251,3064,3065,3073,3080],{},[254,3066,3067,3069,3070],{},[19,3068,3020],{}," → ",[19,3071,3072],{},"\"NotFound\"",[254,3074,3075,3069,3077],{},[19,3076,3037],{},[19,3078,3079],{},"\"InvalidArgument\"",[254,3081,3082,3083],{},"неизвестное → ",[19,3084,3085],{},"\"Internal\"",[3087,3088,3089],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":54,"searchDepth":92,"depth":92,"links":3091},[3092,3093,3096,3097,3098,3099,3100,3101,3102,3103,3104],{"id":40,"depth":92,"text":41},{"id":273,"depth":92,"text":274,"children":3094},[3095],{"id":484,"depth":110,"text":485},{"id":700,"depth":92,"text":701},{"id":993,"depth":92,"text":994},{"id":1405,"depth":92,"text":1406},{"id":1762,"depth":92,"text":1763},{"id":2260,"depth":92,"text":2261},{"id":2485,"depth":92,"text":2486},{"id":2569,"depth":92,"text":2570},{"id":2618,"depth":92,"text":2619},{"id":2683,"depth":92,"text":2684},1781022066789]