Skip to content

[Detail Bug] Directions: Normalizing steps filters nulls and breaks stepIndexes alignment #83

@detail-app

Description

@detail-app

Detail Bug Report

https://app.detail.dev/org_befd6425-a158-4e24-9d4d-1e5c08769515/bugs/bug_8c98bca4-beee-4bfb-b04d-e73e0e81aef4

Introduced in #34 by @WilliamAGH on Jan 23, 2026

Summary

  • Context: DirectionsResponse normalizes Apple Maps API responses. stepIndexes (from DirectionsRoute) contain integer values that index into the steps array.
  • Bug: Null elements are filtered from steps, but stepIndexes values are preserved unchanged. These indices reference positions in the original array, not the filtered array.
  • Actual vs. expected: stepIndexes values should remain valid indices into the normalized steps list, but filtering nulls shifts element positions, rendering indices invalid.
  • Impact: Any consumer using stepIndexes[i] to access steps.get(stepIndexes[i]) will get wrong steps or IndexOutOfBoundsException.

Code with Bug

// DirectionsResponse.java - filters nulls from steps
private static <T> List<T> normalizeList(List<T> rawList) {
    if (rawList == null) {
        return List.of();
    }
    return rawList.stream()
        .filter(Objects::nonNull)  // <-- BUG 🔴 filters nulls, shifting step positions and invalidating stepIndexes
        .toList();
}

// DirectionsRoute.java - stepIndexes values are preserved unchanged
public DirectionsRoute {
    // ...
    stepIndexes = normalizeList(stepIndexes);  // <-- BUG 🔴 only removes null entries from stepIndexes, does not adjust index values
}

Explanation

DirectionsRoute.stepIndexes stores index values into the DirectionsResponse.steps array as returned by the API (which can be sparse). When DirectionsResponse normalizes steps by removing null entries, it changes the positions of subsequent elements. Because stepIndexes values are not recomputed, they can become out-of-bounds or point to the wrong step.

Example:

  • API: steps = [step0, null, step2], stepIndexes = [0, 2]
  • After normalization: steps = [step0, step2] but stepIndexes remains [0, 2], so 2 is invalid for a list of size 2.

Codebase Inconsistency

A prior fix for stepPaths preserved index alignment with steps by converting nulls to placeholders instead of filtering them (commit 9989645). stepIndexes has the same “indexes into array” contract, but steps are still normalized via null filtering, so index alignment is not preserved.

Failing Test

// src/test/java/com/williamcallahan/applemaps/domain/model/DirectionsResponseIndexAlignmentBugTest.java

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.Test;

class DirectionsResponseIndexAlignmentBugTest {

    @Test
    void stepIndexesShouldRemainValidAfterStepsNormalization() {
        DirectionsStep step0 = new DirectionsStep(
            Optional.empty(), Optional.empty(), Optional.empty(),
            Optional.empty(), Optional.empty()
        );
        DirectionsStep step2 = new DirectionsStep(
            Optional.empty(), Optional.empty(), Optional.empty(),
            Optional.empty(), Optional.empty()
        );

        List<DirectionsStep> stepsWithNull = new ArrayList<>();
        stepsWithNull.add(step0);
        stepsWithNull.add(null);  // API returns sparse array
        stepsWithNull.add(step2);

        List<Integer> stepIndexes = List.of(0, 2);  // Values reference original positions

        DirectionsRoute route = new DirectionsRoute(
            Optional.empty(), Optional.empty(), Optional.empty(),
            Optional.empty(), Optional.empty(), Optional.empty(),
            stepIndexes
        );

        DirectionsResponse response = new DirectionsResponse(
            Optional.empty(), Optional.empty(), List.of(route), stepsWithNull, List.of()
        );

        for (Integer stepIndex : response.routes().get(0).stepIndexes()) {
            assertTrue(stepIndex < response.steps().size());
        }
    }
}

Test output:

DirectionsResponseIndexAlignmentBugTest > stepIndexesShouldRemainValidAfterStepsNormalization() FAILED
    org.opentest4j.AssertionFailedError: stepIndex 2 should be valid for steps list of size 2 
    ==> expected: <true> but was: <false>

Recommended Fix

Preserve positional alignment in steps by converting null entries to an explicit placeholder (mirroring the stepPaths approach), instead of filtering:

private static List<DirectionsStep> normalizeSteps(List<DirectionsStep> rawList) {
    if (rawList == null) {
        return List.of();
    }
    return rawList.stream()
        .map(step -> step != null ? step : DirectionsStep.EMPTY)
        .toList();
}

History

This bug was introduced in commit e58b274. That change filtered null elements from lists to address sparse arrays, but did not account for index-aligned relationships between steps and stepIndexes. A later change (commit 9989645) preserved alignment for stepPathssteps, but the same fix pattern was not applied to stepsstepIndexes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions