github.com/99designs/gqlgen@v0.17.45/graphql/handler/transport/http_form_multipart.go (about) 1 package transport 2 3 import ( 4 "encoding/json" 5 "io" 6 "mime" 7 "net/http" 8 "os" 9 10 "github.com/99designs/gqlgen/graphql" 11 ) 12 13 // MultipartForm the Multipart request spec https://github.com/jaydenseric/graphql-multipart-request-spec 14 type MultipartForm struct { 15 // MaxUploadSize sets the maximum number of bytes used to parse a request body 16 // as multipart/form-data. 17 MaxUploadSize int64 18 19 // MaxMemory defines the maximum number of bytes used to parse a request body 20 // as multipart/form-data in memory, with the remainder stored on disk in 21 // temporary files. 22 MaxMemory int64 23 24 // Map of all headers that are added to graphql response. If not 25 // set, only one header: Content-Type: application/json will be set. 26 ResponseHeaders map[string][]string 27 } 28 29 var _ graphql.Transport = MultipartForm{} 30 31 func (f MultipartForm) Supports(r *http.Request) bool { 32 if r.Header.Get("Upgrade") != "" { 33 return false 34 } 35 36 mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) 37 if err != nil { 38 return false 39 } 40 41 return r.Method == "POST" && mediaType == "multipart/form-data" 42 } 43 44 func (f MultipartForm) maxUploadSize() int64 { 45 if f.MaxUploadSize == 0 { 46 return 32 << 20 47 } 48 return f.MaxUploadSize 49 } 50 51 func (f MultipartForm) maxMemory() int64 { 52 if f.MaxMemory == 0 { 53 return 32 << 20 54 } 55 return f.MaxMemory 56 } 57 58 func (f MultipartForm) Do(w http.ResponseWriter, r *http.Request, exec graphql.GraphExecutor) { 59 writeHeaders(w, f.ResponseHeaders) 60 61 start := graphql.Now() 62 63 var err error 64 if r.ContentLength > f.maxUploadSize() { 65 writeJsonError(w, "failed to parse multipart form, request body too large") 66 return 67 } 68 r.Body = http.MaxBytesReader(w, r.Body, f.maxUploadSize()) 69 defer r.Body.Close() 70 71 mr, err := r.MultipartReader() 72 if err != nil { 73 w.WriteHeader(http.StatusUnprocessableEntity) 74 writeJsonError(w, "failed to parse multipart form") 75 return 76 } 77 78 part, err := mr.NextPart() 79 if err != nil || part.FormName() != "operations" { 80 w.WriteHeader(http.StatusUnprocessableEntity) 81 writeJsonError(w, "first part must be operations") 82 return 83 } 84 85 var params graphql.RawParams 86 if err = jsonDecode(part, ¶ms); err != nil { 87 w.WriteHeader(http.StatusUnprocessableEntity) 88 writeJsonError(w, "operations form field could not be decoded") 89 return 90 } 91 92 part, err = mr.NextPart() 93 if err != nil || part.FormName() != "map" { 94 w.WriteHeader(http.StatusUnprocessableEntity) 95 writeJsonError(w, "second part must be map") 96 return 97 } 98 99 uploadsMap := map[string][]string{} 100 if err = json.NewDecoder(part).Decode(&uploadsMap); err != nil { 101 w.WriteHeader(http.StatusUnprocessableEntity) 102 writeJsonError(w, "map form field could not be decoded") 103 return 104 } 105 106 for { 107 part, err = mr.NextPart() 108 if err == io.EOF { 109 break 110 } else if err != nil { 111 w.WriteHeader(http.StatusUnprocessableEntity) 112 writeJsonErrorf(w, "failed to parse part") 113 return 114 } 115 116 key := part.FormName() 117 filename := part.FileName() 118 contentType := part.Header.Get("Content-Type") 119 120 paths := uploadsMap[key] 121 if len(paths) == 0 { 122 w.WriteHeader(http.StatusUnprocessableEntity) 123 writeJsonErrorf(w, "invalid empty operations paths list for key %s", key) 124 return 125 } 126 delete(uploadsMap, key) 127 128 var upload graphql.Upload 129 if r.ContentLength < f.maxMemory() { 130 fileBytes, err := io.ReadAll(part) 131 if err != nil { 132 w.WriteHeader(http.StatusUnprocessableEntity) 133 writeJsonErrorf(w, "failed to read file for key %s", key) 134 return 135 } 136 for _, path := range paths { 137 upload = graphql.Upload{ 138 File: &bytesReader{s: &fileBytes, i: 0}, 139 Size: int64(len(fileBytes)), 140 Filename: filename, 141 ContentType: contentType, 142 } 143 144 if err := params.AddUpload(upload, key, path); err != nil { 145 w.WriteHeader(http.StatusUnprocessableEntity) 146 writeJsonGraphqlError(w, err) 147 return 148 } 149 } 150 } else { 151 tmpFile, err := os.CreateTemp(os.TempDir(), "gqlgen-") 152 if err != nil { 153 w.WriteHeader(http.StatusUnprocessableEntity) 154 writeJsonErrorf(w, "failed to create temp file for key %s", key) 155 return 156 } 157 tmpName := tmpFile.Name() 158 defer func() { 159 _ = os.Remove(tmpName) 160 }() 161 fileSize, err := io.Copy(tmpFile, part) 162 if err != nil { 163 w.WriteHeader(http.StatusUnprocessableEntity) 164 if err := tmpFile.Close(); err != nil { 165 writeJsonErrorf(w, "failed to copy to temp file and close temp file for key %s", key) 166 return 167 } 168 writeJsonErrorf(w, "failed to copy to temp file for key %s", key) 169 return 170 } 171 if err := tmpFile.Close(); err != nil { 172 w.WriteHeader(http.StatusUnprocessableEntity) 173 writeJsonErrorf(w, "failed to close temp file for key %s", key) 174 return 175 } 176 for _, path := range paths { 177 pathTmpFile, err := os.Open(tmpName) 178 if err != nil { 179 w.WriteHeader(http.StatusUnprocessableEntity) 180 writeJsonErrorf(w, "failed to open temp file for key %s", key) 181 return 182 } 183 defer pathTmpFile.Close() 184 upload = graphql.Upload{ 185 File: pathTmpFile, 186 Size: fileSize, 187 Filename: filename, 188 ContentType: contentType, 189 } 190 191 if err := params.AddUpload(upload, key, path); err != nil { 192 w.WriteHeader(http.StatusUnprocessableEntity) 193 writeJsonGraphqlError(w, err) 194 return 195 } 196 } 197 } 198 } 199 200 for key := range uploadsMap { 201 w.WriteHeader(http.StatusUnprocessableEntity) 202 writeJsonErrorf(w, "failed to get key %s from form", key) 203 return 204 } 205 206 params.Headers = r.Header 207 208 params.ReadTime = graphql.TraceTiming{ 209 Start: start, 210 End: graphql.Now(), 211 } 212 213 rc, gerr := exec.CreateOperationContext(r.Context(), ¶ms) 214 if gerr != nil { 215 resp := exec.DispatchError(graphql.WithOperationContext(r.Context(), rc), gerr) 216 w.WriteHeader(statusFor(gerr)) 217 writeJson(w, resp) 218 return 219 } 220 responses, ctx := exec.DispatchOperation(r.Context(), rc) 221 writeJson(w, responses(ctx)) 222 }