Skip to content

Commit 0e168b6

Browse files
committed
fix(geocoder): keep the caller's shape on failure and resolve district results
Two fixes, both verified against a live Photon index. The error fallback now matches the shape the caller's schema declares. Query.geocoder is [Geocoder] and Gym.formatted is String, so returning {} made GraphQL discard the field with "Expected Iterable" or "String cannot represent value" rather than degrading to an empty answer. Surfacing a non-2xx as a throw turned a transient Photon 429 or 500 from an empty list into a discarded field, which is a worse outcome than the silence it replaced. The failure is still reported: it is logged before the fallback returns. That object fallback predates this branch and was already wrong for both callers, but it was only reachable when the URL was missing entirely. Making upstream failures throw is what turned a rare bug into a routine one. The type map now includes district. A suburb searched by name comes back as its own result, with the label in `name` and no `district` field, so the value being searched for was the one value missing from the answer. Live index, q=West Town Chicago: {name: "West Town", type: "district", osm_key: "place", osm_value: "suburb"} now yields suburb "West Town" instead of an empty string. Both regressions fail their tests without the fix: 34/35 without the district entry, 32/35 with the object fallback restored.
1 parent d3d9347 commit 0e168b6

3 files changed

Lines changed: 74 additions & 3 deletions

File tree

server/src/services/geocoder.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,12 @@ async function geocoder(nominatimUrl, search, reverse, format, provider) {
108108
: results
109109
} catch (e) {
110110
log.warn(TAGS.geocoder, 'Unable to geocode for', search, e)
111-
return {}
111+
// The fallback has to match the shape the caller's schema expects.
112+
// Query.geocoder is [Geocoder] and Gym.formatted is String, so returning {}
113+
// made GraphQL discard the field with "Expected Iterable" or "String cannot
114+
// represent value" rather than degrading to an empty answer. The failure is
115+
// still reported: it is logged immediately above.
116+
return reverse ? '' : []
112117
}
113118
}
114119

server/src/services/photonGeocoder.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ const TYPE_TO_PROPERTY = {
6969
state: 'state',
7070
county: 'county',
7171
city: 'city',
72+
// A search for a suburb by name comes back as type=district with the label in
73+
// `name` and no `district` field, so without this the result being asked for
74+
// is the one value missing from the answer. Live index, q=West Town Chicago:
75+
// {name: "West Town", type: "district", osm_key: "place", osm_value: "suburb"}
76+
district: 'district',
7277
locality: 'locality',
7378
street: 'street',
7479
}

server/test/geocoder.test.js

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ test('a Photon URL on the Nominatim provider fails without crashing', async () =
492492
// geocoder() catches and returns {}. What matters is that the process
493493
// survives to get here at all.
494494
const result = await geocoder(server.url, 'Denver', false, '{{city}}')
495-
assert.deepEqual(result, {})
495+
assert.deepEqual(result, [])
496496
} finally {
497497
await server.close()
498498
}
@@ -508,7 +508,7 @@ test('a Nominatim URL on the Photon provider fails without returning nothing sil
508508
'{{city}}',
509509
'photon',
510510
)
511-
assert.deepEqual(result, {})
511+
assert.deepEqual(result, [])
512512
} finally {
513513
await server.close()
514514
}
@@ -747,3 +747,64 @@ test('a correctly configured Photon webhook still reverse geocodes', async () =>
747747
await server.close()
748748
}
749749
})
750+
751+
// Captured from a live index, q=West Town Chicago. A suburb searched by name
752+
// comes back as its own result: the label is in `name` and the containing
753+
// `district` field is absent, so without a district entry in the type map the
754+
// one value being asked for is the one missing from the answer.
755+
test('a district searched by name populates suburb', () => {
756+
const got = formatPhotonFeature(
757+
feature(
758+
{
759+
city: 'West Chicago Township',
760+
country: 'United States',
761+
countrycode: 'US',
762+
county: 'Cook County',
763+
name: 'West Town',
764+
osm_key: 'place',
765+
osm_value: 'suburb',
766+
state: 'Illinois',
767+
type: 'district',
768+
},
769+
[-87.6796, 41.9088],
770+
),
771+
)
772+
assert.equal(got.suburb, 'West Town')
773+
assert.equal(
774+
got.formattedAddress,
775+
'West Town, West Chicago Township, Cook County, Illinois, United States',
776+
)
777+
})
778+
779+
// Query.geocoder is [Geocoder] and Gym.formatted is String. An object fallback
780+
// makes GraphQL discard the field entirely rather than degrade to an empty
781+
// answer, so a transient upstream failure has to keep the caller's shape.
782+
test('a transient upstream failure keeps the shape the schema expects', async () => {
783+
const failing = http.createServer((_, res) => {
784+
res.writeHead(500, { 'Content-Type': 'application/json' })
785+
res.end(JSON.stringify({ message: 'overloaded' }))
786+
})
787+
await new Promise((resolve) => {
788+
failing.listen(0, '127.0.0.1', resolve)
789+
})
790+
const url = `http://127.0.0.1:${failing.address().port}`
791+
try {
792+
const forward = await geocoder(url, 'Denver', false, '{{city}}', 'photon')
793+
assert.ok(Array.isArray(forward), `forward returned ${typeof forward}`)
794+
assert.deepEqual(forward, [])
795+
796+
const reverse = await geocoder(
797+
url,
798+
{ lat: 39.7392, lon: -104.9903 },
799+
true,
800+
'{{city}}',
801+
'photon',
802+
)
803+
assert.equal(typeof reverse, 'string', 'reverse must stay a String')
804+
assert.equal(reverse, '')
805+
} finally {
806+
await new Promise((resolve) => {
807+
failing.close(resolve)
808+
})
809+
}
810+
})

0 commit comments

Comments
 (0)