github.com/prebid/prebid-server/v2@v2.18.0/stored_requests/backends/db_fetcher/fetcher.go (about) 1 package db_fetcher 2 3 import ( 4 "context" 5 "encoding/json" 6 7 "github.com/lib/pq" 8 9 "github.com/golang/glog" 10 "github.com/prebid/prebid-server/v2/stored_requests" 11 "github.com/prebid/prebid-server/v2/stored_requests/backends/db_provider" 12 ) 13 14 func NewFetcher( 15 provider db_provider.DbProvider, 16 queryTemplate string, 17 responseQueryTemplate string, 18 ) stored_requests.AllFetcher { 19 20 if provider == nil { 21 glog.Fatalf("The Database Stored Request Fetcher requires a database connection. Please report this as a bug.") 22 } 23 if queryTemplate == "" { 24 glog.Fatalf("The Database Stored Request Fetcher requires a queryTemplate. Please report this as a bug.") 25 } 26 if responseQueryTemplate == "" { 27 glog.Fatalf("The Database Stored Response Fetcher requires a responseQueryTemplate. Please report this as a bug.") 28 } 29 return &dbFetcher{ 30 provider: provider, 31 queryTemplate: queryTemplate, 32 responseQueryTemplate: responseQueryTemplate, 33 } 34 } 35 36 // dbFetcher fetches Stored Requests from a database. This should be instantiated through the NewFetcher() function. 37 type dbFetcher struct { 38 provider db_provider.DbProvider 39 queryTemplate string 40 responseQueryTemplate string 41 } 42 43 func (fetcher *dbFetcher) FetchRequests(ctx context.Context, requestIDs []string, impIDs []string) (map[string]json.RawMessage, map[string]json.RawMessage, []error) { 44 if len(requestIDs) < 1 && len(impIDs) < 1 { 45 return nil, nil, nil 46 } 47 48 requestIDsParam := make([]interface{}, len(requestIDs)) 49 for i := 0; i < len(requestIDs); i++ { 50 requestIDsParam[i] = requestIDs[i] 51 } 52 impIDsParam := make([]interface{}, len(impIDs)) 53 for i := 0; i < len(impIDs); i++ { 54 impIDsParam[i] = impIDs[i] 55 } 56 57 params := []db_provider.QueryParam{ 58 {Name: "REQUEST_ID_LIST", Value: requestIDsParam}, 59 {Name: "IMP_ID_LIST", Value: impIDsParam}, 60 } 61 62 rows, err := fetcher.provider.QueryContext(ctx, fetcher.queryTemplate, params...) 63 if err != nil { 64 if err != context.DeadlineExceeded && !isBadInput(err) { 65 glog.Errorf("Error reading from Stored Request DB: %s", err.Error()) 66 errs := appendErrors("Request", requestIDs, nil, nil) 67 errs = appendErrors("Imp", impIDs, nil, errs) 68 return nil, nil, errs 69 } 70 return nil, nil, []error{err} 71 } 72 defer func() { 73 if err := rows.Close(); err != nil { 74 glog.Errorf("error closing DB connection: %v", err) 75 } 76 }() 77 78 storedRequestData := make(map[string]json.RawMessage, len(requestIDs)) 79 storedImpData := make(map[string]json.RawMessage, len(impIDs)) 80 for rows.Next() { 81 var id string 82 var data []byte 83 var dataType string 84 85 // Fixes #338 86 if err := rows.Scan(&id, &data, &dataType); err != nil { 87 return nil, nil, []error{err} 88 } 89 90 switch dataType { 91 case "request": 92 storedRequestData[id] = data 93 case "imp": 94 storedImpData[id] = data 95 default: 96 glog.Errorf("Database result set with id=%s has invalid type: %s. This will be ignored.", id, dataType) 97 } 98 } 99 100 // Fixes #338 101 if rows.Err() != nil { 102 return nil, nil, []error{rows.Err()} 103 } 104 105 errs := appendErrors("Request", requestIDs, storedRequestData, nil) 106 errs = appendErrors("Imp", impIDs, storedImpData, errs) 107 108 return storedRequestData, storedImpData, errs 109 } 110 111 func (fetcher *dbFetcher) FetchResponses(ctx context.Context, ids []string) (data map[string]json.RawMessage, errs []error) { 112 if len(ids) < 1 { 113 return nil, nil 114 } 115 116 idInterfaces := make([]interface{}, len(ids)) 117 for i := 0; i < len(ids); i++ { 118 idInterfaces[i] = ids[i] 119 } 120 params := []db_provider.QueryParam{ 121 {Name: "ID_LIST", Value: idInterfaces}, 122 } 123 124 rows, err := fetcher.provider.QueryContext(ctx, fetcher.responseQueryTemplate, params...) 125 if err != nil { 126 return nil, []error{err} 127 } 128 defer func() { 129 if err := rows.Close(); err != nil { 130 glog.Errorf("error closing DB connection: %v", err) 131 } 132 }() 133 134 storedData := make(map[string]json.RawMessage, len(ids)) 135 for rows.Next() { 136 var id string 137 var data []byte 138 var dataType string 139 140 if err := rows.Scan(&id, &data, &dataType); err != nil { 141 return nil, []error{err} 142 } 143 storedData[id] = data 144 } 145 146 if rows.Err() != nil { 147 return nil, []error{rows.Err()} 148 } 149 150 return storedData, errs 151 152 } 153 154 func (fetcher *dbFetcher) FetchAccount(ctx context.Context, accountDefaultsJSON json.RawMessage, accountID string) (json.RawMessage, []error) { 155 return nil, []error{stored_requests.NotFoundError{ID: accountID, DataType: "Account"}} 156 } 157 158 func (fetcher *dbFetcher) FetchCategories(ctx context.Context, primaryAdServer, publisherId, iabCategory string) (string, error) { 159 return "", nil 160 } 161 162 func appendErrors(dataType string, ids []string, data map[string]json.RawMessage, errs []error) []error { 163 for _, id := range ids { 164 if _, ok := data[id]; !ok { 165 errs = append(errs, stored_requests.NotFoundError{ 166 ID: id, 167 DataType: dataType, 168 }) 169 } 170 } 171 return errs 172 } 173 174 // Returns true if the Postgres error signifies some sort of bad user input, and false otherwise. 175 // 176 // These errors are documented here: https://www.postgresql.org/docs/9.3/static/errcodes-appendix.html 177 func isBadInput(err error) bool { 178 // Unfortunately, Postgres queries will fail if a non-UUID is passed into a query for a UUID column. For example: 179 // 180 // SELECT uuid, data, dataType FROM stored_requests WHERE uuid IN ('abc'); 181 // 182 // Since users can send us strings which are _not_ UUIDs, and we don't want the code to assume anything about 183 // the database schema, we can just convert these into standard NotFoundErrors. 184 if pqErr, ok := err.(*pq.Error); ok && string(pqErr.Code) == "22P02" { 185 return true 186 } 187 188 return false 189 }