Add "Cancel" action to proxy intercept

This commit is contained in:
David Stotijn
2022-03-14 09:12:36 +01:00
parent 9dd8464af7
commit e1067ecffb
8 changed files with 297 additions and 10 deletions

View File

@ -1,3 +1,4 @@
import CancelIcon from "@mui/icons-material/Cancel";
import SendIcon from "@mui/icons-material/Send"; import SendIcon from "@mui/icons-material/Send";
import { Alert, Box, Button, CircularProgress, Typography } from "@mui/material"; import { Alert, Box, Button, CircularProgress, Typography } from "@mui/material";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -12,6 +13,7 @@ import UrlBar, { HttpMethod, HttpProto, httpProtoMap } from "lib/components/UrlB
import { import {
HttpProtocol, HttpProtocol,
HttpRequest, HttpRequest,
useCancelRequestMutation,
useGetInterceptedRequestQuery, useGetInterceptedRequestQuery,
useModifyRequestMutation, useModifyRequestMutation,
} from "lib/graphql/generated"; } from "lib/graphql/generated";
@ -27,7 +29,6 @@ function EditRequest(): JSX.Element {
// If there's no request selected and there are pending reqs, navigate to // If there's no request selected and there are pending reqs, navigate to
// the first one in the list. This helps you quickly review/handle reqs // the first one in the list. This helps you quickly review/handle reqs
// without having to manually select the next one in the requests table. // without having to manually select the next one in the requests table.
console.log(router.isReady, router.query.id, interceptedRequests?.length);
if (router.isReady && !router.query.id && interceptedRequests?.length) { if (router.isReady && !router.query.id && interceptedRequests?.length) {
const req = interceptedRequests[0]; const req = interceptedRequests[0];
router.replace(`/proxy/intercept?id=${req.id}`); router.replace(`/proxy/intercept?id=${req.id}`);
@ -104,6 +105,16 @@ function EditRequest(): JSX.Element {
const interceptedReq = reqId ? getReqResult?.data?.interceptedRequest : undefined; const interceptedReq = reqId ? getReqResult?.data?.interceptedRequest : undefined;
const [modifyRequest, modifyResult] = useModifyRequestMutation(); const [modifyRequest, modifyResult] = useModifyRequestMutation();
const [cancelRequest, cancelResult] = useCancelRequestMutation();
const onActionCompleted = () => {
setURL("");
setMethod(HttpMethod.Get);
setBody("");
setQueryParams([]);
setHeaders([]);
router.replace(`/proxy/intercept`);
};
const handleFormSubmit: React.FormEventHandler = (e) => { const handleFormSubmit: React.FormEventHandler = (e) => {
e.preventDefault(); e.preventDefault();
@ -132,15 +143,29 @@ function EditRequest(): JSX.Element {
}, },
}); });
}, },
onCompleted: () => { onCompleted: onActionCompleted,
setURL(""); });
setMethod(HttpMethod.Get); };
setBody("");
setQueryParams([]); const handleCancelClick = () => {
setHeaders([]); if (!interceptedReq) {
console.log("done!"); return;
router.replace(`/proxy/intercept`); }
cancelRequest({
variables: {
id: interceptedReq.id,
}, },
update(cache) {
cache.modify({
fields: {
interceptedRequests(existing: HttpRequest[], { readField }) {
return existing.filter((ref) => interceptedReq.id !== readField("id", ref));
},
},
});
},
onCompleted: onActionCompleted,
}); });
}; };
@ -161,17 +186,32 @@ function EditRequest(): JSX.Element {
variant="contained" variant="contained"
disableElevation disableElevation
type="submit" type="submit"
disabled={!interceptedReq || modifyResult.loading} disabled={!interceptedReq || modifyResult.loading || cancelResult.loading}
startIcon={modifyResult.loading ? <CircularProgress size={22} /> : <SendIcon />} startIcon={modifyResult.loading ? <CircularProgress size={22} /> : <SendIcon />}
> >
Send Send
</Button> </Button>
<Button
variant="contained"
color="error"
disableElevation
onClick={handleCancelClick}
disabled={!interceptedReq || modifyResult.loading || cancelResult.loading}
startIcon={cancelResult.loading ? <CircularProgress size={22} /> : <CancelIcon />}
>
Cancel
</Button>
</Box> </Box>
{modifyResult.error && ( {modifyResult.error && (
<Alert severity="error" sx={{ mt: 1 }}> <Alert severity="error" sx={{ mt: 1 }}>
{modifyResult.error.message} {modifyResult.error.message}
</Alert> </Alert>
)} )}
{cancelResult.error && (
<Alert severity="error" sx={{ mt: 1 }}>
{cancelResult.error.message}
</Alert>
)}
</Box> </Box>
<Box flex="1 auto" position="relative"> <Box flex="1 auto" position="relative">

View File

@ -0,0 +1,5 @@
mutation CancelRequest($id: ID!) {
cancelRequest(id: $id) {
success
}
}

View File

@ -18,6 +18,11 @@ export type Scalars = {
URL: any; URL: any;
}; };
export type CancelRequestResult = {
__typename?: 'CancelRequestResult';
success: Scalars['Boolean'];
};
export type ClearHttpRequestLogResult = { export type ClearHttpRequestLogResult = {
__typename?: 'ClearHTTPRequestLogResult'; __typename?: 'ClearHTTPRequestLogResult';
success: Scalars['Boolean']; success: Scalars['Boolean'];
@ -127,6 +132,7 @@ export type ModifyRequestResult = {
export type Mutation = { export type Mutation = {
__typename?: 'Mutation'; __typename?: 'Mutation';
cancelRequest: CancelRequestResult;
clearHTTPRequestLog: ClearHttpRequestLogResult; clearHTTPRequestLog: ClearHttpRequestLogResult;
closeProject: CloseProjectResult; closeProject: CloseProjectResult;
createOrUpdateSenderRequest: SenderRequest; createOrUpdateSenderRequest: SenderRequest;
@ -143,6 +149,11 @@ export type Mutation = {
}; };
export type MutationCancelRequestArgs = {
id: Scalars['ID'];
};
export type MutationCreateOrUpdateSenderRequestArgs = { export type MutationCreateOrUpdateSenderRequestArgs = {
request: SenderRequestInput; request: SenderRequestInput;
}; };
@ -285,6 +296,13 @@ export type SenderRequestInput = {
url: Scalars['URL']; url: Scalars['URL'];
}; };
export type CancelRequestMutationVariables = Exact<{
id: Scalars['ID'];
}>;
export type CancelRequestMutation = { __typename?: 'Mutation', cancelRequest: { __typename?: 'CancelRequestResult', success: boolean } };
export type GetInterceptedRequestQueryVariables = Exact<{ export type GetInterceptedRequestQueryVariables = Exact<{
id: Scalars['ID']; id: Scalars['ID'];
}>; }>;
@ -410,6 +428,39 @@ export type GetInterceptedRequestsQueryVariables = Exact<{ [key: string]: never;
export type GetInterceptedRequestsQuery = { __typename?: 'Query', interceptedRequests: Array<{ __typename?: 'HttpRequest', id: string, url: any, method: HttpMethod }> }; export type GetInterceptedRequestsQuery = { __typename?: 'Query', interceptedRequests: Array<{ __typename?: 'HttpRequest', id: string, url: any, method: HttpMethod }> };
export const CancelRequestDocument = gql`
mutation CancelRequest($id: ID!) {
cancelRequest(id: $id) {
success
}
}
`;
export type CancelRequestMutationFn = Apollo.MutationFunction<CancelRequestMutation, CancelRequestMutationVariables>;
/**
* __useCancelRequestMutation__
*
* To run a mutation, you first call `useCancelRequestMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCancelRequestMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [cancelRequestMutation, { data, loading, error }] = useCancelRequestMutation({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useCancelRequestMutation(baseOptions?: Apollo.MutationHookOptions<CancelRequestMutation, CancelRequestMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CancelRequestMutation, CancelRequestMutationVariables>(CancelRequestDocument, options);
}
export type CancelRequestMutationHookResult = ReturnType<typeof useCancelRequestMutation>;
export type CancelRequestMutationResult = Apollo.MutationResult<CancelRequestMutation>;
export type CancelRequestMutationOptions = Apollo.BaseMutationOptions<CancelRequestMutation, CancelRequestMutationVariables>;
export const GetInterceptedRequestDocument = gql` export const GetInterceptedRequestDocument = gql`
query GetInterceptedRequest($id: ID!) { query GetInterceptedRequest($id: ID!) {
interceptedRequest(id: $id) { interceptedRequest(id: $id) {

View File

@ -45,6 +45,10 @@ type DirectiveRoot struct {
} }
type ComplexityRoot struct { type ComplexityRoot struct {
CancelRequestResult struct {
Success func(childComplexity int) int
}
ClearHTTPRequestLogResult struct { ClearHTTPRequestLogResult struct {
Success func(childComplexity int) int Success func(childComplexity int) int
} }
@ -105,6 +109,7 @@ type ComplexityRoot struct {
} }
Mutation struct { Mutation struct {
CancelRequest func(childComplexity int, id ulid.ULID) int
ClearHTTPRequestLog func(childComplexity int) int ClearHTTPRequestLog func(childComplexity int) int
CloseProject func(childComplexity int) int CloseProject func(childComplexity int) int
CreateOrUpdateSenderRequest func(childComplexity int, request SenderRequestInput) int CreateOrUpdateSenderRequest func(childComplexity int, request SenderRequestInput) int
@ -182,6 +187,7 @@ type MutationResolver interface {
SendRequest(ctx context.Context, id ulid.ULID) (*SenderRequest, error) SendRequest(ctx context.Context, id ulid.ULID) (*SenderRequest, error)
DeleteSenderRequests(ctx context.Context) (*DeleteSenderRequestsResult, error) DeleteSenderRequests(ctx context.Context) (*DeleteSenderRequestsResult, error)
ModifyRequest(ctx context.Context, request ModifyRequestInput) (*ModifyRequestResult, error) ModifyRequest(ctx context.Context, request ModifyRequestInput) (*ModifyRequestResult, error)
CancelRequest(ctx context.Context, id ulid.ULID) (*CancelRequestResult, error)
} }
type QueryResolver interface { type QueryResolver interface {
HTTPRequestLog(ctx context.Context, id ulid.ULID) (*HTTPRequestLog, error) HTTPRequestLog(ctx context.Context, id ulid.ULID) (*HTTPRequestLog, error)
@ -211,6 +217,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
_ = ec _ = ec
switch typeName + "." + field { switch typeName + "." + field {
case "CancelRequestResult.success":
if e.complexity.CancelRequestResult.Success == nil {
break
}
return e.complexity.CancelRequestResult.Success(childComplexity), true
case "ClearHTTPRequestLogResult.success": case "ClearHTTPRequestLogResult.success":
if e.complexity.ClearHTTPRequestLogResult.Success == nil { if e.complexity.ClearHTTPRequestLogResult.Success == nil {
break break
@ -414,6 +427,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ModifyRequestResult.Success(childComplexity), true return e.complexity.ModifyRequestResult.Success(childComplexity), true
case "Mutation.cancelRequest":
if e.complexity.Mutation.CancelRequest == nil {
break
}
args, err := ec.field_Mutation_cancelRequest_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.CancelRequest(childComplexity, args["id"].(ulid.ULID)), true
case "Mutation.clearHTTPRequestLog": case "Mutation.clearHTTPRequestLog":
if e.complexity.Mutation.ClearHTTPRequestLog == nil { if e.complexity.Mutation.ClearHTTPRequestLog == nil {
break break
@ -977,6 +1002,10 @@ type ModifyRequestResult {
success: Boolean! success: Boolean!
} }
type CancelRequestResult {
success: Boolean!
}
type Query { type Query {
httpRequestLog(id: ID!): HttpRequestLog httpRequestLog(id: ID!): HttpRequestLog
httpRequestLogs: [HttpRequestLog!]! httpRequestLogs: [HttpRequestLog!]!
@ -1006,6 +1035,7 @@ type Mutation {
sendRequest(id: ID!): SenderRequest! sendRequest(id: ID!): SenderRequest!
deleteSenderRequests: DeleteSenderRequestsResult! deleteSenderRequests: DeleteSenderRequestsResult!
modifyRequest(request: ModifyRequestInput!): ModifyRequestResult! modifyRequest(request: ModifyRequestInput!): ModifyRequestResult!
cancelRequest(id: ID!): CancelRequestResult!
} }
enum HttpMethod { enum HttpMethod {
@ -1037,6 +1067,21 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...)
// region ***************************** args.gotpl ***************************** // region ***************************** args.gotpl *****************************
func (ec *executionContext) field_Mutation_cancelRequest_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 ulid.ULID
if tmp, ok := rawArgs["id"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
arg0, err = ec.unmarshalNID2githubᚗcomᚋoklogᚋulidᚐULID(ctx, tmp)
if err != nil {
return nil, err
}
}
args["id"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_createOrUpdateSenderRequest_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Mutation_createOrUpdateSenderRequest_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@ -1285,6 +1330,41 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg
// region **************************** field.gotpl ***************************** // region **************************** field.gotpl *****************************
func (ec *executionContext) _CancelRequestResult_success(ctx context.Context, field graphql.CollectedField, obj *CancelRequestResult) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "CancelRequestResult",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Success, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _ClearHTTPRequestLogResult_success(ctx context.Context, field graphql.CollectedField, obj *ClearHTTPRequestLogResult) (ret graphql.Marshaler) { func (ec *executionContext) _ClearHTTPRequestLogResult_success(ctx context.Context, field graphql.CollectedField, obj *ClearHTTPRequestLogResult) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -2798,6 +2878,48 @@ func (ec *executionContext) _Mutation_modifyRequest(ctx context.Context, field g
return ec.marshalNModifyRequestResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyRequestResult(ctx, field.Selections, res) return ec.marshalNModifyRequestResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyRequestResult(ctx, field.Selections, res)
} }
func (ec *executionContext) _Mutation_cancelRequest(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_cancelRequest_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().CancelRequest(rctx, args["id"].(ulid.ULID))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*CancelRequestResult)
fc.Result = res
return ec.marshalNCancelRequestResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCancelRequestResult(ctx, field.Selections, res)
}
func (ec *executionContext) _Project_id(ctx context.Context, field graphql.CollectedField, obj *Project) (ret graphql.Marshaler) { func (ec *executionContext) _Project_id(ctx context.Context, field graphql.CollectedField, obj *Project) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -5279,6 +5401,33 @@ func (ec *executionContext) unmarshalInputSenderRequestInput(ctx context.Context
// region **************************** object.gotpl **************************** // region **************************** object.gotpl ****************************
var cancelRequestResultImplementors = []string{"CancelRequestResult"}
func (ec *executionContext) _CancelRequestResult(ctx context.Context, sel ast.SelectionSet, obj *CancelRequestResult) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, cancelRequestResultImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("CancelRequestResult")
case "success":
out.Values[i] = ec._CancelRequestResult_success(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var clearHTTPRequestLogResultImplementors = []string{"ClearHTTPRequestLogResult"} var clearHTTPRequestLogResultImplementors = []string{"ClearHTTPRequestLogResult"}
func (ec *executionContext) _ClearHTTPRequestLogResult(ctx context.Context, sel ast.SelectionSet, obj *ClearHTTPRequestLogResult) graphql.Marshaler { func (ec *executionContext) _ClearHTTPRequestLogResult(ctx context.Context, sel ast.SelectionSet, obj *ClearHTTPRequestLogResult) graphql.Marshaler {
@ -5697,6 +5846,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "cancelRequest":
out.Values[i] = ec._Mutation_cancelRequest(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@ -6303,6 +6457,20 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se
return res return res
} }
func (ec *executionContext) marshalNCancelRequestResult2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCancelRequestResult(ctx context.Context, sel ast.SelectionSet, v CancelRequestResult) graphql.Marshaler {
return ec._CancelRequestResult(ctx, sel, &v)
}
func (ec *executionContext) marshalNCancelRequestResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCancelRequestResult(ctx context.Context, sel ast.SelectionSet, v *CancelRequestResult) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._CancelRequestResult(ctx, sel, v)
}
func (ec *executionContext) marshalNClearHTTPRequestLogResult2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐClearHTTPRequestLogResult(ctx context.Context, sel ast.SelectionSet, v ClearHTTPRequestLogResult) graphql.Marshaler { func (ec *executionContext) marshalNClearHTTPRequestLogResult2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐClearHTTPRequestLogResult(ctx context.Context, sel ast.SelectionSet, v ClearHTTPRequestLogResult) graphql.Marshaler {
return ec._ClearHTTPRequestLogResult(ctx, sel, &v) return ec._ClearHTTPRequestLogResult(ctx, sel, &v)
} }

View File

@ -12,6 +12,10 @@ import (
"github.com/oklog/ulid" "github.com/oklog/ulid"
) )
type CancelRequestResult struct {
Success bool `json:"success"`
}
type ClearHTTPRequestLogResult struct { type ClearHTTPRequestLogResult struct {
Success bool `json:"success"` Success bool `json:"success"`
} }

View File

@ -581,6 +581,15 @@ func (r *mutationResolver) ModifyRequest(ctx context.Context, input ModifyReques
return &ModifyRequestResult{Success: true}, nil return &ModifyRequestResult{Success: true}, nil
} }
func (r *mutationResolver) CancelRequest(ctx context.Context, id ulid.ULID) (*CancelRequestResult, error) {
err := r.InterceptService.CancelRequest(id)
if err != nil {
return nil, fmt.Errorf("could not cancel http request: %w", err)
}
return &CancelRequestResult{Success: true}, nil
}
func parseSenderRequest(req sender.Request) (SenderRequest, error) { func parseSenderRequest(req sender.Request) (SenderRequest, error) {
method := HTTPMethod(req.Method) method := HTTPMethod(req.Method)
if method != "" && !method.IsValid() { if method != "" && !method.IsValid() {

View File

@ -138,6 +138,10 @@ type ModifyRequestResult {
success: Boolean! success: Boolean!
} }
type CancelRequestResult {
success: Boolean!
}
type Query { type Query {
httpRequestLog(id: ID!): HttpRequestLog httpRequestLog(id: ID!): HttpRequestLog
httpRequestLogs: [HttpRequestLog!]! httpRequestLogs: [HttpRequestLog!]!
@ -167,6 +171,7 @@ type Mutation {
sendRequest(id: ID!): SenderRequest! sendRequest(id: ID!): SenderRequest!
deleteSenderRequests: DeleteSenderRequestsResult! deleteSenderRequests: DeleteSenderRequestsResult!
modifyRequest(request: ModifyRequestInput!): ModifyRequestResult! modifyRequest(request: ModifyRequestInput!): ModifyRequestResult!
cancelRequest(id: ID!): CancelRequestResult!
} }
enum HttpMethod { enum HttpMethod {

View File

@ -144,6 +144,11 @@ func (svc *Service) ModifyRequest(reqID ulid.ULID, modReq *http.Request) error {
} }
} }
// CancelRequest ensures an intercepted request is dropped.
func (svc *Service) CancelRequest(reqID ulid.ULID) error {
return svc.ModifyRequest(reqID, nil)
}
func (svc *Service) ClearRequests() { func (svc *Service) ClearRequests() {
svc.mu.Lock() svc.mu.Lock()
defer svc.mu.Unlock() defer svc.mu.Unlock()